From 269e026381b37d3e3348ee7b08bf77cbd8bcc98a Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Tue, 4 Mar 2025 15:57:26 +0100 Subject: [PATCH 001/411] fix: sonarqube action not running in merge queue (#4861) --- .github/workflows/sonarqube.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 1104dbbd0f..d5ee858164 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -6,6 +6,7 @@ on: - main pull_request: types: [opened, synchronize, reopened] + merge_group: permissions: contents: read jobs: From 40d54d60d43a0dfe5c3eec2e3bfdf46c3468f1b2 Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Tue, 4 Mar 2025 06:57:41 -0800 Subject: [PATCH 002/411] docs: Release test environment docs (#4842) --- .../core-features/test-environment/modal.webp | Bin 0 -> 22074 bytes .../test-environment/more-actions.webp | Bin 0 -> 15446 bytes .../test-environment/test-env.webp | Bin 0 -> 23352 bytes .../test-environment/toggle.webp | Bin 0 -> 10178 bytes docs/mint.json | 3 +- .../core-features/test-environment.mdx | 39 ++++++++++++++++++ 6 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 docs/images/xm-and-surveys/core-features/test-environment/modal.webp create mode 100644 docs/images/xm-and-surveys/core-features/test-environment/more-actions.webp create mode 100644 docs/images/xm-and-surveys/core-features/test-environment/test-env.webp create mode 100644 docs/images/xm-and-surveys/core-features/test-environment/toggle.webp create mode 100644 docs/xm-and-surveys/core-features/test-environment.mdx diff --git a/docs/images/xm-and-surveys/core-features/test-environment/modal.webp b/docs/images/xm-and-surveys/core-features/test-environment/modal.webp new file mode 100644 index 0000000000000000000000000000000000000000..e85bad83d78ab3885dc676ffc835a2f3bf521487 GIT binary patch literal 22074 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 literal 0 HcmV?d00001 diff --git a/docs/images/xm-and-surveys/core-features/test-environment/toggle.webp b/docs/images/xm-and-surveys/core-features/test-environment/toggle.webp new file mode 100644 index 0000000000000000000000000000000000000000..7048b71ea32d883d7993cce47a5209dd17eaab68 GIT binary patch literal 10178 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 + + Click on the "More actions" button of a survey in the survey list + + ![More actions](/images/xm-and-surveys/core-features/test-environment/more-actions.webp) + + + + Choose which environment you want to copy the survey to + + ![Copy survey](/images/xm-and-surveys/core-features/test-environment/modal.webp) + + From 0e898db710fd85f2578a5c90f1e0779d12d9617f Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:28:00 +0530 Subject: [PATCH 003/411] chore: Remove lib dependency from survey package (#4767) Co-authored-by: Piyush Gupta --- .../src/scripts/generate-data-migration.ts | 2 +- .../database/src/scripts/migration-runner.ts | 2 +- packages/lib/utils/colors.ts | 30 +- packages/lib/utils/datetime.ts | 57 +-- packages/lib/utils/videoUpload.ts | 4 +- .../src/components/survey-web-view.tsx | 6 +- .../src/components/general/ending-card.tsx | 2 +- .../src/components/general/file-input.tsx | 4 +- .../components/general/language-switch.tsx | 2 +- .../general/question-conditional.tsx | 2 +- .../src/components/general/question-media.tsx | 7 +- .../general/response-error-component.tsx | 2 +- .../surveys/src/components/general/survey.tsx | 2 +- .../src/components/general/welcome-card.tsx | 2 +- .../components/questions/address-question.tsx | 2 +- .../src/components/questions/cal-question.tsx | 2 +- .../components/questions/consent-question.tsx | 2 +- .../questions/contact-info-question.tsx | 2 +- .../src/components/questions/cta-question.tsx | 2 +- .../components/questions/date-question.tsx | 4 +- .../questions/file-upload-question.tsx | 2 +- .../components/questions/matrix-question.tsx | 2 +- .../multiple-choice-multi-question.tsx | 2 +- .../multiple-choice-single-question.tsx | 2 +- .../src/components/questions/nps-question.tsx | 2 +- .../questions/open-text-question.tsx | 2 +- .../questions/picture-selection-question.tsx | 4 +- .../components/questions/ranking-question.tsx | 2 +- .../components/questions/rating-question.tsx | 2 +- .../components/wrappers/survey-container.tsx | 2 +- packages/surveys/src/lib/color.ts | 53 ++ packages/surveys/src/lib/date-time.ts | 48 ++ packages/surveys/src/lib/i18n.ts | 19 + packages/surveys/src/lib/logic.ts | 473 ++++++++++++++++++ packages/surveys/src/lib/recall.ts | 38 +- packages/surveys/src/lib/response.ts | 28 ++ packages/surveys/src/lib/storage.ts | 22 + packages/surveys/src/lib/styles.ts | 2 +- .../surveys/src/lib/use-click-outside-hook.ts | 36 ++ packages/surveys/src/lib/utils.ts | 38 +- packages/surveys/src/lib/video-upload.ts | 127 +++++ 41 files changed, 889 insertions(+), 155 deletions(-) create mode 100644 packages/surveys/src/lib/color.ts create mode 100644 packages/surveys/src/lib/date-time.ts create mode 100644 packages/surveys/src/lib/i18n.ts create mode 100644 packages/surveys/src/lib/logic.ts create mode 100644 packages/surveys/src/lib/response.ts create mode 100644 packages/surveys/src/lib/storage.ts create mode 100644 packages/surveys/src/lib/use-click-outside-hook.ts create mode 100644 packages/surveys/src/lib/video-upload.ts diff --git a/packages/database/src/scripts/generate-data-migration.ts b/packages/database/src/scripts/generate-data-migration.ts index a9ea29b00c..dfd923829b 100644 --- a/packages/database/src/scripts/generate-data-migration.ts +++ b/packages/database/src/scripts/generate-data-migration.ts @@ -1,7 +1,7 @@ -import { createId } from "@paralleldrive/cuid2"; import fs from "node:fs/promises"; import path from "node:path"; import readline from "node:readline"; +import { createId } from "@paralleldrive/cuid2"; const rl = readline.createInterface({ input: process.stdin, diff --git a/packages/database/src/scripts/migration-runner.ts b/packages/database/src/scripts/migration-runner.ts index 5a48553c90..b0d020ec6b 100644 --- a/packages/database/src/scripts/migration-runner.ts +++ b/packages/database/src/scripts/migration-runner.ts @@ -1,8 +1,8 @@ -import { type Prisma, PrismaClient } from "@prisma/client"; import { exec } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; +import { type Prisma, PrismaClient } from "@prisma/client"; const execAsync = promisify(exec); diff --git a/packages/lib/utils/colors.ts b/packages/lib/utils/colors.ts index 9f11e68947..5f8ba6d343 100644 --- a/packages/lib/utils/colors.ts +++ b/packages/lib/utils/colors.ts @@ -1,4 +1,4 @@ -export const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => { +const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => { // return undefined if hex is undefined, this is important for adding the default values to the CSS variables // TODO: find a better way to handle this if (!hex || hex === "") return undefined; @@ -17,34 +17,6 @@ export const hexToRGBA = (hex: string | undefined, opacity: number): string | un return `rgba(${r}, ${g}, ${b}, ${opacity})`; }; -export const lightenDarkenColor = (hexColor: string, magnitude: number): string => { - hexColor = hexColor.replace(`#`, ``); - - // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") - if (hexColor.length === 3) { - hexColor = hexColor - .split("") - .map((char) => char + char) - .join(""); - } - - if (hexColor.length === 6) { - let decimalColor = parseInt(hexColor, 16); - let r = (decimalColor >> 16) + magnitude; - r = Math.max(0, Math.min(255, r)); // Clamp value between 0 and 255 - let g = ((decimalColor >> 8) & 0x00ff) + magnitude; - g = Math.max(0, Math.min(255, g)); // Clamp value between 0 and 255 - let b = (decimalColor & 0x0000ff) + magnitude; - b = Math.max(0, Math.min(255, b)); // Clamp value between 0 and 255 - - // Convert back to hex and return - return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - } else { - // Return the original color if it's neither 3 nor 6 characters - return hexColor; - } -}; - export const mixColor = (hexColor: string, mixWithHex: string, weight: number): string => { // Convert both colors to RGBA format const color1 = hexToRGBA(hexColor, 1) || ""; diff --git a/packages/lib/utils/datetime.ts b/packages/lib/utils/datetime.ts index f86a91b79a..1f3d866081 100644 --- a/packages/lib/utils/datetime.ts +++ b/packages/lib/utils/datetime.ts @@ -1,17 +1,8 @@ -const monthNames = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", -]; +const getOrdinalSuffix = (day: number) => { + const suffixes = ["th", "st", "nd", "rd"]; + const relevantDigits = day < 30 ? day % 20 : day % 30; + return suffixes[relevantDigits <= 3 ? relevantDigits : 0]; +}; // Helper function to calculate difference in days between two dates export const diffInDays = (date1: Date, date2: Date) => { @@ -19,42 +10,12 @@ export const diffInDays = (date1: Date, date2: Date) => { return Math.floor(diffTime / (1000 * 60 * 60 * 24)); }; -// Helper function to get the month name -export const getMonthName = (monthIndex: number) => { - return monthNames[monthIndex]; -}; - -export const formatDateWithOrdinal = (date: Date): string => { - const getOrdinalSuffix = (day: number) => { - const suffixes = ["th", "st", "nd", "rd"]; - const relevantDigits = day < 30 ? day % 20 : day % 30; - return suffixes[relevantDigits <= 3 ? relevantDigits : 0]; - }; - - const dayOfWeekNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - - const dayOfWeek = dayOfWeekNames[date.getDay()]; +export const formatDateWithOrdinal = (date: Date, locale: string = "en-US"): string => { + const dayOfWeek = new Intl.DateTimeFormat(locale, { weekday: "long" }).format(date); const day = date.getDate(); - const monthIndex = date.getMonth(); + const month = new Intl.DateTimeFormat(locale, { month: "long" }).format(date); const year = date.getFullYear(); - - return `${dayOfWeek}, ${monthNames[monthIndex]} ${day}${getOrdinalSuffix(day)}, ${year}`; -}; - -// Helper function to format the date with an ordinal suffix -export const getOrdinalDate = (date: number) => { - const j = date % 10, - k = date % 100; - if (j === 1 && k !== 11) { - return date + "st"; - } - if (j === 2 && k !== 12) { - return date + "nd"; - } - if (j === 3 && k !== 13) { - return date + "rd"; - } - return date + "th"; + return `${dayOfWeek}, ${month} ${day}${getOrdinalSuffix(day)}, ${year}`; }; export const isValidDateString = (value: string) => { diff --git a/packages/lib/utils/videoUpload.ts b/packages/lib/utils/videoUpload.ts index 36563ca74c..bae60fc30b 100644 --- a/packages/lib/utils/videoUpload.ts +++ b/packages/lib/utils/videoUpload.ts @@ -21,7 +21,7 @@ export const checkForYoutubeUrl = (url: string): boolean => { } }; -export const checkForVimeoUrl = (url: string): boolean => { +const checkForVimeoUrl = (url: string): boolean => { try { const vimeoUrl = new URL(url); @@ -37,7 +37,7 @@ export const checkForVimeoUrl = (url: string): boolean => { } }; -export const checkForLoomUrl = (url: string): boolean => { +const checkForLoomUrl = (url: string): boolean => { try { const loomUrl = new URL(url); diff --git a/packages/react-native/src/components/survey-web-view.tsx b/packages/react-native/src/components/survey-web-view.tsx index 98144ce1c0..a7e81bdc01 100644 --- a/packages/react-native/src/components/survey-web-view.tsx +++ b/packages/react-native/src/components/survey-web-view.tsx @@ -1,13 +1,13 @@ /* eslint-disable no-console -- debugging*/ +import React, { type JSX, useEffect, useRef, useState } from "react"; +import { Modal } from "react-native"; +import { WebView, type WebViewMessageEvent } from "react-native-webview"; import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { filterSurveys, getLanguageCode, getStyling } from "@/lib/common/utils"; import { SurveyStore } from "@/lib/survey/store"; import { type TEnvironmentStateSurvey, type TUserState, ZJsRNWebViewOnMessageData } from "@/types/config"; import type { SurveyContainerProps } from "@/types/survey"; -import React, { type JSX, useEffect, useRef, useState } from "react"; -import { Modal } from "react-native"; -import { WebView, type WebViewMessageEvent } from "react-native-webview"; const appConfig = RNConfig.getInstance(); const logger = Logger.getInstance(); diff --git a/packages/surveys/src/components/general/ending-card.tsx b/packages/surveys/src/components/general/ending-card.tsx index a6e4cc01aa..fe12494586 100644 --- a/packages/surveys/src/components/general/ending-card.tsx +++ b/packages/surveys/src/components/general/ending-card.tsx @@ -4,9 +4,9 @@ import { LoadingSpinner } from "@/components/general/loading-spinner"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { replaceRecallInfo } from "@/lib/recall"; import { useEffect } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js"; import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses"; import { type TSurveyEndScreenCard, type TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/general/file-input.tsx b/packages/surveys/src/components/general/file-input.tsx index d02c3f7985..fb6ef9aee6 100644 --- a/packages/surveys/src/components/general/file-input.tsx +++ b/packages/surveys/src/components/general/file-input.tsx @@ -1,8 +1,8 @@ +import { getOriginalFileNameFromUrl } from "@/lib/storage"; +import { isFulfilled, isRejected } from "@/lib/utils"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useMemo, useState } from "preact/hooks"; import { type JSXInternal } from "preact/src/jsx"; -import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils"; -import { isFulfilled, isRejected } from "@formbricks/lib/utils/promises"; import { type TAllowedFileExtension } from "@formbricks/types/common"; import { type TJsFileUploadParams } from "@formbricks/types/js"; import { type TUploadFileConfig } from "@formbricks/types/storage"; diff --git a/packages/surveys/src/components/general/language-switch.tsx b/packages/surveys/src/components/general/language-switch.tsx index 96f9839ed6..7b23cdb7ce 100644 --- a/packages/surveys/src/components/general/language-switch.tsx +++ b/packages/surveys/src/components/general/language-switch.tsx @@ -1,5 +1,5 @@ import { GlobeIcon } from "@/components/general/globe-icon"; -import { useClickOutside } from "@/lib/utils"; +import { useClickOutside } from "@/lib/use-click-outside-hook"; import { useRef, useState } from "preact/hooks"; import { getLanguageLabel } from "@formbricks/lib/i18n/utils"; import { type TSurveyLanguage } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/general/question-conditional.tsx b/packages/surveys/src/components/general/question-conditional.tsx index b67be6f3c7..2e56ceedf3 100644 --- a/packages/surveys/src/components/general/question-conditional.tsx +++ b/packages/surveys/src/components/general/question-conditional.tsx @@ -13,7 +13,7 @@ import { OpenTextQuestion } from "@/components/questions/open-text-question"; import { PictureSelectionQuestion } from "@/components/questions/picture-selection-question"; import { RankingQuestion } from "@/components/questions/ranking-question"; import { RatingQuestion } from "@/components/questions/rating-question"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; +import { getLocalizedValue } from "@/lib/i18n"; import { type TJsFileUploadParams } from "@formbricks/types/js"; import { type TResponseData, type TResponseDataValue, type TResponseTtc } from "@formbricks/types/responses"; import { type TUploadFileConfig } from "@formbricks/types/storage"; diff --git a/packages/surveys/src/components/general/question-media.tsx b/packages/surveys/src/components/general/question-media.tsx index 5ec21a9862..91bd97f76c 100644 --- a/packages/surveys/src/components/general/question-media.tsx +++ b/packages/surveys/src/components/general/question-media.tsx @@ -1,10 +1,5 @@ +import { checkForLoomUrl, checkForVimeoUrl, checkForYoutubeUrl, convertToEmbedUrl } from "@/lib/video-upload"; import { useState } from "preact/hooks"; -import { - checkForLoomUrl, - checkForVimeoUrl, - checkForYoutubeUrl, - convertToEmbedUrl, -} from "@formbricks/lib/utils/videoUpload"; //Function to add extra params to videoUrls in order to reduce video controls const getVideoUrlWithParams = (videoUrl: string): string => { diff --git a/packages/surveys/src/components/general/response-error-component.tsx b/packages/surveys/src/components/general/response-error-component.tsx index b29f4b8c38..b9a2370bcd 100644 --- a/packages/surveys/src/components/general/response-error-component.tsx +++ b/packages/surveys/src/components/general/response-error-component.tsx @@ -1,5 +1,5 @@ import { SubmitButton } from "@/components/buttons/submit-button"; -import { processResponseData } from "@formbricks/lib/responses"; +import { processResponseData } from "@/lib/response"; import { type TResponseData } from "@formbricks/types/responses"; import { type TSurveyQuestion } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/general/survey.tsx b/packages/surveys/src/components/general/survey.tsx index d2748bf238..0ac7c4796b 100644 --- a/packages/surveys/src/components/general/survey.tsx +++ b/packages/surveys/src/components/general/survey.tsx @@ -9,13 +9,13 @@ import { WelcomeCard } from "@/components/general/welcome-card"; import { AutoCloseWrapper } from "@/components/wrappers/auto-close-wrapper"; import { StackedCardsContainer } from "@/components/wrappers/stacked-cards-container"; import { ApiClient } from "@/lib/api-client"; +import { evaluateLogic, performActions } from "@/lib/logic"; import { parseRecallInformation } from "@/lib/recall"; import { ResponseQueue } from "@/lib/response-queue"; import { SurveyState } from "@/lib/survey-state"; import { cn, getDefaultLanguageCode } from "@/lib/utils"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; import { type JSX, useCallback } from "react"; -import { evaluateLogic, performActions } from "@formbricks/lib/surveyLogic/utils"; import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys"; import { type TJsEnvironmentStateSurvey, TJsFileUploadParams } from "@formbricks/types/js"; import type { diff --git a/packages/surveys/src/components/general/welcome-card.tsx b/packages/surveys/src/components/general/welcome-card.tsx index 72989c375b..4c8d4e3fa3 100644 --- a/packages/surveys/src/components/general/welcome-card.tsx +++ b/packages/surveys/src/components/general/welcome-card.tsx @@ -1,9 +1,9 @@ import { SubmitButton } from "@/components/buttons/submit-button"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { replaceRecallInfo } from "@/lib/recall"; import { calculateElementIdx } from "@/lib/utils"; import { useEffect } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js"; import { type TResponseData, type TResponseTtc, type TResponseVariables } from "@formbricks/types/responses"; import { type TI18nString } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/address-question.tsx b/packages/surveys/src/components/questions/address-question.tsx index 388e3f39f3..cca80aa04c 100644 --- a/packages/surveys/src/components/questions/address-question.tsx +++ b/packages/surveys/src/components/questions/address-question.tsx @@ -5,10 +5,10 @@ import { Input } from "@/components/general/input"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { useMemo, useRef, useState } from "preact/hooks"; import { useCallback } from "react"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyAddressQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/cal-question.tsx b/packages/surveys/src/components/questions/cal-question.tsx index 4067e86649..675b246868 100644 --- a/packages/surveys/src/components/questions/cal-question.tsx +++ b/packages/surveys/src/components/questions/cal-question.tsx @@ -5,9 +5,9 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { useCallback, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import { type TSurveyCalQuestion, type TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/consent-question.tsx b/packages/surveys/src/components/questions/consent-question.tsx index e5b1a5a822..9d03d4dda0 100644 --- a/packages/surveys/src/components/questions/consent-question.tsx +++ b/packages/surveys/src/components/questions/consent-question.tsx @@ -4,9 +4,9 @@ import { Headline } from "@/components/general/headline"; import { HtmlBody } from "@/components/general/html-body"; import { QuestionMedia } from "@/components/general/question-media"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { useCallback, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyConsentQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/contact-info-question.tsx b/packages/surveys/src/components/questions/contact-info-question.tsx index f33f26a2e0..edfad42a99 100644 --- a/packages/surveys/src/components/questions/contact-info-question.tsx +++ b/packages/surveys/src/components/questions/contact-info-question.tsx @@ -5,9 +5,9 @@ import { Input } from "@/components/general/input"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { useCallback, useMemo, useRef, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyContactInfoQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/cta-question.tsx b/packages/surveys/src/components/questions/cta-question.tsx index 5172688895..2d4a669d43 100644 --- a/packages/surveys/src/components/questions/cta-question.tsx +++ b/packages/surveys/src/components/questions/cta-question.tsx @@ -4,9 +4,9 @@ import { Headline } from "@/components/general/headline"; import { HtmlBody } from "@/components/general/html-body"; import { QuestionMedia } from "@/components/general/question-media"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { useState } from "react"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyCTAQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/date-question.tsx b/packages/surveys/src/components/questions/date-question.tsx index 37aa42f7bb..0a8b58559d 100644 --- a/packages/surveys/src/components/questions/date-question.tsx +++ b/packages/surveys/src/components/questions/date-question.tsx @@ -4,13 +4,13 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getMonthName, getOrdinalDate } from "@/lib/date-time"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn } from "@/lib/utils"; import { useEffect, useMemo, useState } from "preact/hooks"; import DatePicker from "react-date-picker"; import { DatePickerProps } from "react-date-picker"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { getMonthName, getOrdinalDate } from "@formbricks/lib/utils/datetime"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; import "../../styles/date-picker.css"; diff --git a/packages/surveys/src/components/questions/file-upload-question.tsx b/packages/surveys/src/components/questions/file-upload-question.tsx index c12d7842d6..e757fb7172 100644 --- a/packages/surveys/src/components/questions/file-upload-question.tsx +++ b/packages/surveys/src/components/questions/file-upload-question.tsx @@ -2,9 +2,9 @@ import { SubmitButton } from "@/components/buttons/submit-button"; import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TJsFileUploadParams } from "@formbricks/types/js"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import { type TUploadFileConfig } from "@formbricks/types/storage"; diff --git a/packages/surveys/src/components/questions/matrix-question.tsx b/packages/surveys/src/components/questions/matrix-question.tsx index d6d9d6197e..98fc65a11d 100644 --- a/packages/surveys/src/components/questions/matrix-question.tsx +++ b/packages/surveys/src/components/questions/matrix-question.tsx @@ -4,11 +4,11 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { getShuffledRowIndices } from "@/lib/utils"; import { type JSX } from "preact"; import { useCallback, useMemo, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TI18nString, TSurveyMatrixQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx b/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx index 337ac169bc..dc46955feb 100644 --- a/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx @@ -4,10 +4,10 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn, getShuffledChoicesIds } from "@/lib/utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/multiple-choice-single-question.tsx b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx index 56b213f062..bac793834e 100644 --- a/packages/surveys/src/components/questions/multiple-choice-single-question.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx @@ -4,10 +4,10 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn, getShuffledChoicesIds } from "@/lib/utils"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/nps-question.tsx b/packages/surveys/src/components/questions/nps-question.tsx index 42a152084d..95fb806e1a 100644 --- a/packages/surveys/src/components/questions/nps-question.tsx +++ b/packages/surveys/src/components/questions/nps-question.tsx @@ -4,10 +4,10 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn } from "@/lib/utils"; import { useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyNPSQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/open-text-question.tsx b/packages/surveys/src/components/questions/open-text-question.tsx index ac94413592..20ccbea7ab 100644 --- a/packages/surveys/src/components/questions/open-text-question.tsx +++ b/packages/surveys/src/components/questions/open-text-question.tsx @@ -4,10 +4,10 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { type RefObject } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyOpenTextQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/picture-selection-question.tsx b/packages/surveys/src/components/questions/picture-selection-question.tsx index f6dda3cd70..363d183274 100644 --- a/packages/surveys/src/components/questions/picture-selection-question.tsx +++ b/packages/surveys/src/components/questions/picture-selection-question.tsx @@ -4,11 +4,11 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; +import { getOriginalFileNameFromUrl } from "@/lib/storage"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn } from "@/lib/utils"; import { useEffect, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyPictureSelectionQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; diff --git a/packages/surveys/src/components/questions/ranking-question.tsx b/packages/surveys/src/components/questions/ranking-question.tsx index 301a59c983..8bf68b239c 100644 --- a/packages/surveys/src/components/questions/ranking-question.tsx +++ b/packages/surveys/src/components/questions/ranking-question.tsx @@ -4,11 +4,11 @@ import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { Subheader } from "@/components/general/subheader"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn, getShuffledChoicesIds } from "@/lib/utils"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useCallback, useMemo, useState } from "preact/hooks"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyQuestionChoice, diff --git a/packages/surveys/src/components/questions/rating-question.tsx b/packages/surveys/src/components/questions/rating-question.tsx index c93200dc40..d81825a227 100644 --- a/packages/surveys/src/components/questions/rating-question.tsx +++ b/packages/surveys/src/components/questions/rating-question.tsx @@ -3,11 +3,11 @@ import { SubmitButton } from "@/components/buttons/submit-button"; import { Headline } from "@/components/general/headline"; import { QuestionMedia } from "@/components/general/question-media"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; +import { getLocalizedValue } from "@/lib/i18n"; import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { cn } from "@/lib/utils"; import { useEffect, useState } from "preact/hooks"; import type { JSX } from "react"; -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses"; import type { TSurveyQuestionId, TSurveyRatingQuestion } from "@formbricks/types/surveys/types"; import { diff --git a/packages/surveys/src/components/wrappers/survey-container.tsx b/packages/surveys/src/components/wrappers/survey-container.tsx index dd23d8f4d9..6096394cb6 100644 --- a/packages/surveys/src/components/wrappers/survey-container.tsx +++ b/packages/surveys/src/components/wrappers/survey-container.tsx @@ -1,5 +1,5 @@ +import { cn } from "@/lib/utils"; import { useEffect, useRef, useState } from "preact/hooks"; -import { cn } from "@formbricks/lib/cn"; import { type TPlacement } from "@formbricks/types/common"; interface SurveyContainerProps { diff --git a/packages/surveys/src/lib/color.ts b/packages/surveys/src/lib/color.ts new file mode 100644 index 0000000000..5f8ba6d343 --- /dev/null +++ b/packages/surveys/src/lib/color.ts @@ -0,0 +1,53 @@ +const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => { + // return undefined if hex is undefined, this is important for adding the default values to the CSS variables + // TODO: find a better way to handle this + if (!hex || hex === "") return undefined; + + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, (_, r, g, b) => r + r + g + g + b + b); + + let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (!result) return ""; + + let r = parseInt(result[1], 16); + let g = parseInt(result[2], 16); + let b = parseInt(result[3], 16); + + return `rgba(${r}, ${g}, ${b}, ${opacity})`; +}; + +export const mixColor = (hexColor: string, mixWithHex: string, weight: number): string => { + // Convert both colors to RGBA format + const color1 = hexToRGBA(hexColor, 1) || ""; + const color2 = hexToRGBA(mixWithHex, 1) || ""; + + // Extract RGBA values + const [r1, g1, b1] = color1.match(/\d+/g)?.map(Number) || [0, 0, 0]; + const [r2, g2, b2] = color2.match(/\d+/g)?.map(Number) || [0, 0, 0]; + + // Mix the colors + const r = Math.round(r1 * (1 - weight) + r2 * weight); + const g = Math.round(g1 * (1 - weight) + g2 * weight); + const b = Math.round(b1 * (1 - weight) + b2 * weight); + + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; +}; + +export const isLight = (color: string) => { + let r: number | undefined, g: number | undefined, b: number | undefined; + + if (color.length === 4) { + r = parseInt(color[1] + color[1], 16); + g = parseInt(color[2] + color[2], 16); + b = parseInt(color[3] + color[3], 16); + } else if (color.length === 7) { + r = parseInt(color[1] + color[2], 16); + g = parseInt(color[3] + color[4], 16); + b = parseInt(color[5] + color[6], 16); + } + if (r === undefined || g === undefined || b === undefined) { + throw new Error("Invalid color"); + } + return r * 0.299 + g * 0.587 + b * 0.114 > 128; +}; diff --git a/packages/surveys/src/lib/date-time.ts b/packages/surveys/src/lib/date-time.ts new file mode 100644 index 0000000000..457ccf3c1b --- /dev/null +++ b/packages/surveys/src/lib/date-time.ts @@ -0,0 +1,48 @@ +// Helper function to get the month name +export const getMonthName = (monthIndex: number, locale: string = "en-US") => { + if (monthIndex < 0 || monthIndex > 11) { + throw new Error("Month index must be between 0 and 11"); + } + return new Intl.DateTimeFormat(locale, { month: "long" }).format(new Date(2000, monthIndex, 1)); +}; + +// Helper function to format the date with an ordinal suffix +export const getOrdinalDate = (date: number) => { + const j = date % 10, + k = date % 100; + if (j === 1 && k !== 11) { + return date + "st"; + } + if (j === 2 && k !== 12) { + return date + "nd"; + } + if (j === 3 && k !== 13) { + return date + "rd"; + } + return date + "th"; +}; + +export const isValidDateString = (value: string) => { + const regex = /^(?:\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4})$/; + + if (!regex.test(value)) { + return false; + } + + const date = new Date(value); + return !isNaN(date.getTime()); +}; + +const getOrdinalSuffix = (day: number): string => { + const suffixes = ["th", "st", "nd", "rd"]; + const relevantDigits = day < 30 ? day % 20 : day % 30; + return suffixes[relevantDigits <= 3 ? relevantDigits : 0]; +}; + +export const formatDateWithOrdinal = (date: Date, locale: string = "en-US"): string => { + const dayOfWeek = new Intl.DateTimeFormat(locale, { weekday: "long" }).format(date); + const day = date.getDate(); + const month = new Intl.DateTimeFormat(locale, { month: "long" }).format(date); + const year = date.getFullYear(); + return `${dayOfWeek}, ${month} ${day}${getOrdinalSuffix(day)}, ${year}`; +}; diff --git a/packages/surveys/src/lib/i18n.ts b/packages/surveys/src/lib/i18n.ts new file mode 100644 index 0000000000..371b50e3ec --- /dev/null +++ b/packages/surveys/src/lib/i18n.ts @@ -0,0 +1,19 @@ +import { TI18nString } from "@formbricks/types/surveys/types"; + +// Type guard to check if an object is an I18nString +const isI18nObject = (obj: any): obj is TI18nString => { + return typeof obj === "object" && obj !== null && Object.keys(obj).includes("default"); +}; + +export const getLocalizedValue = (value: TI18nString | undefined, languageId: string): string => { + if (!value) { + return ""; + } + if (isI18nObject(value)) { + if (value[languageId]) { + return value[languageId]; + } + return value.default; + } + return ""; +}; diff --git a/packages/surveys/src/lib/logic.ts b/packages/surveys/src/lib/logic.ts new file mode 100644 index 0000000000..54c84da65d --- /dev/null +++ b/packages/surveys/src/lib/logic.ts @@ -0,0 +1,473 @@ +import { getLocalizedValue } from "@/lib/i18n"; +import { TJsEnvironmentStateSurvey } from "@formbricks/types/js"; +import { TResponseData, TResponseVariables } from "@formbricks/types/responses"; +import { + TActionCalculate, + TConditionGroup, + TSingleCondition, + TSurveyLogicAction, + TSurveyQuestion, + TSurveyQuestionTypeEnum, + TSurveyVariable, +} from "@formbricks/types/surveys/types"; + +const getVariableValue = ( + variables: TSurveyVariable[], + variableId: string, + variablesData: TResponseVariables +) => { + const variable = variables.find((v) => v.id === variableId); + if (!variable) return undefined; + const variableValue = variablesData[variableId]; + return variable.type === "number" ? Number(variableValue) || 0 : variableValue || ""; +}; + +type TCondition = TSingleCondition | TConditionGroup; + +export const isConditionGroup = (condition: TCondition): condition is TConditionGroup => { + return (condition as TConditionGroup).connector !== undefined; +}; + +export const evaluateLogic = ( + localSurvey: TJsEnvironmentStateSurvey, + data: TResponseData, + variablesData: TResponseVariables, + conditions: TConditionGroup, + selectedLanguage: string +): boolean => { + const evaluateConditionGroup = (group: TConditionGroup): boolean => { + const results = group.conditions.map((condition) => { + if (isConditionGroup(condition)) { + return evaluateConditionGroup(condition); + } else { + return evaluateSingleCondition(localSurvey, data, variablesData, condition, selectedLanguage); + } + }); + + return group.connector === "or" ? results.some((r) => r) : results.every((r) => r); + }; + + return evaluateConditionGroup(conditions); +}; + +export const performActions = ( + survey: TJsEnvironmentStateSurvey, + actions: TSurveyLogicAction[], + data: TResponseData, + calculationResults: TResponseVariables +): { + jumpTarget: string | undefined; + requiredQuestionIds: string[]; + calculations: TResponseVariables; +} => { + let jumpTarget: string | undefined; + const requiredQuestionIds: string[] = []; + const calculations: TResponseVariables = { ...calculationResults }; + + actions.forEach((action) => { + switch (action.objective) { + case "calculate": + const result = performCalculation(survey, action, data, calculations); + if (result !== undefined) calculations[action.variableId] = result; + break; + case "requireAnswer": + requiredQuestionIds.push(action.target); + break; + case "jumpToQuestion": + if (!jumpTarget) { + jumpTarget = action.target; + } + break; + } + }); + + return { jumpTarget, requiredQuestionIds, calculations }; +}; + +const getLeftOperandValue = ( + localSurvey: TJsEnvironmentStateSurvey, + data: TResponseData, + variablesData: TResponseVariables, + leftOperand: TSingleCondition["leftOperand"], + selectedLanguage: string +) => { + switch (leftOperand.type) { + case "question": + const currentQuestion = localSurvey.questions.find((q) => q.id === leftOperand.value); + if (!currentQuestion) return undefined; + + const responseValue = data[leftOperand.value]; + + if (currentQuestion.type === "openText" && currentQuestion.inputType === "number") { + return Number(responseValue) || undefined; + } + + if (currentQuestion.type === "multipleChoiceSingle" || currentQuestion.type === "multipleChoiceMulti") { + const isOthersEnabled = currentQuestion.choices.at(-1)?.id === "other"; + + if (typeof responseValue === "string") { + const choice = currentQuestion.choices.find((choice) => { + return getLocalizedValue(choice.label, selectedLanguage) === responseValue; + }); + + if (!choice) { + if (isOthersEnabled) { + return "other"; + } + + return undefined; + } + + return choice.id; + } else if (Array.isArray(responseValue)) { + let choices: string[] = []; + responseValue.forEach((value) => { + const foundChoice = currentQuestion.choices.find((choice) => { + return getLocalizedValue(choice.label, selectedLanguage) === value; + }); + + if (foundChoice) { + choices.push(foundChoice.id); + } else if (isOthersEnabled) { + choices.push("other"); + } + }); + if (choices) { + return Array.from(new Set(choices)); + } + } + } + + return data[leftOperand.value]; + case "variable": + const variables = localSurvey.variables || []; + return getVariableValue(variables, leftOperand.value, variablesData); + case "hiddenField": + return data[leftOperand.value]; + default: + return undefined; + } +}; + +const getRightOperandValue = ( + localSurvey: TJsEnvironmentStateSurvey, + data: TResponseData, + variablesData: TResponseVariables, + rightOperand: TSingleCondition["rightOperand"] +) => { + if (!rightOperand) return undefined; + + switch (rightOperand.type) { + case "question": + return data[rightOperand.value]; + case "variable": + const variables = localSurvey.variables || []; + return getVariableValue(variables, rightOperand.value, variablesData); + case "hiddenField": + return data[rightOperand.value]; + case "static": + return rightOperand.value; + default: + return undefined; + } +}; + +const evaluateSingleCondition = ( + localSurvey: TJsEnvironmentStateSurvey, + data: TResponseData, + variablesData: TResponseVariables, + condition: TSingleCondition, + selectedLanguage: string +): boolean => { + try { + let leftValue = getLeftOperandValue( + localSurvey, + data, + variablesData, + condition.leftOperand, + selectedLanguage + ); + let rightValue = condition.rightOperand + ? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand) + : undefined; + + let leftField: TSurveyQuestion | TSurveyVariable | string; + + if (condition.leftOperand?.type === "question") { + leftField = localSurvey.questions.find((q) => q.id === condition.leftOperand?.value) as TSurveyQuestion; + } else if (condition.leftOperand?.type === "variable") { + leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable; + } else if (condition.leftOperand?.type === "hiddenField") { + leftField = condition.leftOperand.value as string; + } else { + leftField = ""; + } + + let rightField: TSurveyQuestion | TSurveyVariable | string; + + if (condition.rightOperand?.type === "question") { + rightField = localSurvey.questions.find( + (q) => q.id === condition.rightOperand?.value + ) as TSurveyQuestion; + } else if (condition.rightOperand?.type === "variable") { + rightField = localSurvey.variables.find( + (v) => v.id === condition.rightOperand?.value + ) as TSurveyVariable; + } else if (condition.rightOperand?.type === "hiddenField") { + rightField = condition.rightOperand.value as string; + } else { + rightField = ""; + } + + if ( + condition.leftOperand.type === "variable" && + (leftField as TSurveyVariable).type === "number" && + condition.rightOperand?.type === "hiddenField" + ) { + rightValue = Number(rightValue as string); + } + + switch (condition.operator) { + case "equals": + if (condition.leftOperand.type === "question") { + if ( + (leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date && + typeof leftValue === "string" && + typeof rightValue === "string" + ) { + // when left value is of date question and right value is string + return new Date(leftValue).getTime() === new Date(rightValue).getTime(); + } + } + + // when left value is of openText, hiddenField, variable and right value is of multichoice + if (condition.rightOperand?.type === "question") { + if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) { + if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { + return rightValue.includes(leftValue as string); + } else return false; + } else if ( + (rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date && + typeof leftValue === "string" && + typeof rightValue === "string" + ) { + return new Date(leftValue).getTime() === new Date(rightValue).getTime(); + } + } + + return ( + (Array.isArray(leftValue) && + leftValue.length === 1 && + typeof rightValue === "string" && + leftValue.includes(rightValue)) || + leftValue === rightValue + ); + case "doesNotEqual": + // when left value is of picture selection question and right value is its option + if ( + condition.leftOperand.type === "question" && + (leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.PictureSelection && + Array.isArray(leftValue) && + leftValue.length > 0 && + typeof rightValue === "string" + ) { + return !leftValue.includes(rightValue); + } + + // when left value is of date question and right value is string + if ( + condition.leftOperand.type === "question" && + (leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date && + typeof leftValue === "string" && + typeof rightValue === "string" + ) { + return new Date(leftValue).getTime() !== new Date(rightValue).getTime(); + } + + // when left value is of openText, hiddenField, variable and right value is of multichoice + if (condition.rightOperand?.type === "question") { + if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) { + if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) { + return !rightValue.includes(leftValue as string); + } else return false; + } else if ( + (rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date && + typeof leftValue === "string" && + typeof rightValue === "string" + ) { + return new Date(leftValue).getTime() !== new Date(rightValue).getTime(); + } + } + + return ( + (Array.isArray(leftValue) && + leftValue.length === 1 && + typeof rightValue === "string" && + !leftValue.includes(rightValue)) || + leftValue !== rightValue + ); + case "contains": + return String(leftValue).includes(String(rightValue)); + case "doesNotContain": + return !String(leftValue).includes(String(rightValue)); + case "startsWith": + return String(leftValue).startsWith(String(rightValue)); + case "doesNotStartWith": + return !String(leftValue).startsWith(String(rightValue)); + case "endsWith": + return String(leftValue).endsWith(String(rightValue)); + case "doesNotEndWith": + return !String(leftValue).endsWith(String(rightValue)); + case "isSubmitted": + if (typeof leftValue === "string") { + if ( + condition.leftOperand.type === "question" && + (leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload && + leftValue + ) { + return leftValue !== "skipped"; + } + return leftValue !== "" && leftValue !== null; + } else if (Array.isArray(leftValue)) { + return leftValue.length > 0; + } else if (typeof leftValue === "number") { + return leftValue !== null; + } + return false; + case "isSkipped": + return ( + (Array.isArray(leftValue) && leftValue.length === 0) || + leftValue === "" || + leftValue === null || + leftValue === undefined || + (typeof leftValue === "object" && Object.entries(leftValue).length === 0) + ); + case "isGreaterThan": + return Number(leftValue) > Number(rightValue); + case "isLessThan": + return Number(leftValue) < Number(rightValue); + case "isGreaterThanOrEqual": + return Number(leftValue) >= Number(rightValue); + case "isLessThanOrEqual": + return Number(leftValue) <= Number(rightValue); + case "equalsOneOf": + return Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.includes(leftValue); + case "includesAllOf": + return ( + Array.isArray(leftValue) && + Array.isArray(rightValue) && + rightValue.every((v) => leftValue.includes(v)) + ); + case "includesOneOf": + return ( + Array.isArray(leftValue) && + Array.isArray(rightValue) && + rightValue.some((v) => leftValue.includes(v)) + ); + case "doesNotIncludeAllOf": + return ( + Array.isArray(leftValue) && + Array.isArray(rightValue) && + rightValue.every((v) => !leftValue.includes(v)) + ); + case "doesNotIncludeOneOf": + return ( + Array.isArray(leftValue) && + Array.isArray(rightValue) && + rightValue.some((v) => !leftValue.includes(v)) + ); + case "isAccepted": + return leftValue === "accepted"; + case "isClicked": + return leftValue === "clicked"; + case "isAfter": + return new Date(String(leftValue)) > new Date(String(rightValue)); + case "isBefore": + return new Date(String(leftValue)) < new Date(String(rightValue)); + case "isBooked": + return leftValue === "booked" || !!(leftValue && leftValue !== ""); + case "isPartiallySubmitted": + if (typeof leftValue === "object") { + return Object.values(leftValue).includes(""); + } else return false; + case "isCompletelySubmitted": + if (typeof leftValue === "object") { + const values = Object.values(leftValue); + return values.length > 0 && !values.includes(""); + } else return false; + default: + return false; + } + } catch (e) { + return false; + } +}; + +const performCalculation = ( + survey: TJsEnvironmentStateSurvey, + action: TActionCalculate, + data: TResponseData, + calculations: Record +): number | string | undefined => { + const variables = survey.variables || []; + const variable = variables.find((v) => v.id === action.variableId); + + if (!variable) return undefined; + + let currentValue = calculations[action.variableId]; + if (currentValue === undefined) { + currentValue = variable.type === "number" ? 0 : ""; + } + let operandValue: string | number | undefined; + + // Determine the operand value based on the action.value type + switch (action.value.type) { + case "static": + operandValue = action.value.value; + break; + case "variable": + const value = calculations[action.value.value]; + if (typeof value === "number" || typeof value === "string") { + operandValue = value; + } + break; + case "question": + case "hiddenField": + const val = data[action.value.value]; + if (typeof val === "number" || typeof val === "string") { + if (variable.type === "number" && !isNaN(Number(val))) { + operandValue = Number(val); + } + operandValue = val; + } + break; + } + + if (operandValue === undefined || operandValue === null) return undefined; + + let result: number | string; + + switch (action.operator) { + case "add": + result = Number(currentValue) + Number(operandValue); + break; + case "subtract": + result = Number(currentValue) - Number(operandValue); + break; + case "multiply": + result = Number(currentValue) * Number(operandValue); + break; + case "divide": + if (Number(operandValue) === 0) return undefined; + result = Number(currentValue) / Number(operandValue); + break; + case "assign": + result = operandValue; + break; + case "concat": + result = String(currentValue) + String(operandValue); + break; + } + + return result; +}; diff --git a/packages/surveys/src/lib/recall.ts b/packages/surveys/src/lib/recall.ts index 89aa16d4e2..e2e78f2081 100644 --- a/packages/surveys/src/lib/recall.ts +++ b/packages/surveys/src/lib/recall.ts @@ -1,10 +1,38 @@ -import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; -import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone"; -import { formatDateWithOrdinal, isValidDateString } from "@formbricks/lib/utils/datetime"; -import { extractFallbackValue, extractId, extractRecallInfo } from "@formbricks/lib/utils/recall"; +import { formatDateWithOrdinal, isValidDateString } from "@/lib/date-time"; +import { getLocalizedValue } from "@/lib/i18n"; import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses"; import { type TSurveyQuestion } from "@formbricks/types/surveys/types"; +// Extracts the ID of recall question from a string containing the "recall" pattern. +const extractId = (text: string): string | null => { + const pattern = /#recall:([A-Za-z0-9_-]+)/; + const match = text.match(pattern); + if (match && match[1]) { + return match[1]; + } else { + return null; + } +}; + +// Extracts the fallback value from a string containing the "fallback" pattern. +const extractFallbackValue = (text: string): string => { + const pattern = /fallback:(\S*)#/; + const match = text.match(pattern); + if (match && match[1]) { + return match[1]; + } else { + return ""; + } +}; + +// Extracts the complete recall information (ID and fallback) from a headline string. +const extractRecallInfo = (headline: string, id?: string): string | null => { + const idPattern = id ? id : "[A-Za-z0-9_-]+"; + const pattern = new RegExp(`#recall:(${idPattern})\\/fallback:(\\S*)#`); + const match = headline.match(pattern); + return match ? match[0] : null; +}; + export const replaceRecallInfo = ( text: string, responseData: TResponseData, @@ -54,7 +82,7 @@ export const parseRecallInformation = ( responseData: TResponseData, variables: TResponseVariables ) => { - const modifiedQuestion = structuredClone(question); + const modifiedQuestion = JSON.parse(JSON.stringify(question)); if (question.headline[languageCode].includes("recall:")) { modifiedQuestion.headline[languageCode] = replaceRecallInfo( getLocalizedValue(modifiedQuestion.headline, languageCode), diff --git a/packages/surveys/src/lib/response.ts b/packages/surveys/src/lib/response.ts new file mode 100644 index 0000000000..c61fc73d8f --- /dev/null +++ b/packages/surveys/src/lib/response.ts @@ -0,0 +1,28 @@ +export const processResponseData = ( + responseData: string | number | string[] | Record +): string => { + switch (typeof responseData) { + case "string": + return responseData; + + case "number": + return responseData.toString(); + + case "object": + if (Array.isArray(responseData)) { + responseData = responseData + .filter((item) => item !== null && item !== undefined && item !== "") + .join(", "); + return responseData; + } else { + const formattedString = Object.entries(responseData) + .filter(([_, value]) => value !== "") + .map(([key, value]) => `${key}: ${value}`) + .join("\n"); + return formattedString; + } + + default: + return ""; + } +}; diff --git a/packages/surveys/src/lib/storage.ts b/packages/surveys/src/lib/storage.ts new file mode 100644 index 0000000000..5b0a902977 --- /dev/null +++ b/packages/surveys/src/lib/storage.ts @@ -0,0 +1,22 @@ +export const getOriginalFileNameFromUrl = (fileURL: string): string => { + try { + const fileNameFromURL = fileURL.startsWith("/storage/") + ? fileURL.split("/").pop() + : new URL(fileURL).pathname.split("/").pop(); + + const fileExt = fileNameFromURL?.split(".").pop() ?? ""; + const originalFileName = fileNameFromURL?.split("--fid--")[0] ?? ""; + const fileId = fileNameFromURL?.split("--fid--")[1] ?? ""; + + if (!fileId) { + const fileName = originalFileName ? decodeURIComponent(originalFileName || "") : ""; + return fileName; + } + + const fileName = originalFileName ? decodeURIComponent(`${originalFileName}.${fileExt}` || "") : ""; + return fileName; + } catch (error) { + console.error(`Error parsing file URL: ${error}`); + return ""; + } +}; diff --git a/packages/surveys/src/lib/styles.ts b/packages/surveys/src/lib/styles.ts index c18d067e84..67eca2599d 100644 --- a/packages/surveys/src/lib/styles.ts +++ b/packages/surveys/src/lib/styles.ts @@ -1,8 +1,8 @@ +import { isLight, mixColor } from "@/lib/color"; import global from "@/styles/global.css?inline"; import preflight from "@/styles/preflight.css?inline"; import calendarCss from "react-calendar/dist/Calendar.css?inline"; import datePickerCss from "react-date-picker/dist/DatePicker.css?inline"; -import { isLight, mixColor } from "@formbricks/lib/utils/colors"; import { type TProjectStyling } from "@formbricks/types/project"; import { type TSurveyStyling } from "@formbricks/types/surveys/types"; import editorCss from "../../../../apps/web/modules/ui/components/editor/styles-editor-frontend.css?inline"; diff --git a/packages/surveys/src/lib/use-click-outside-hook.ts b/packages/surveys/src/lib/use-click-outside-hook.ts new file mode 100644 index 0000000000..d79af49a90 --- /dev/null +++ b/packages/surveys/src/lib/use-click-outside-hook.ts @@ -0,0 +1,36 @@ +import { MutableRef, useEffect } from "preact/hooks"; + +// Improved version of https://usehooks.com/useOnClickOutside/ +export const useClickOutside = ( + ref: MutableRef, + handler: (event: MouseEvent | TouchEvent) => void +): void => { + useEffect(() => { + let startedInside = false; + let startedWhenMounted = false; + + const listener = (event: MouseEvent | TouchEvent) => { + // Do nothing if `mousedown` or `touchstart` started inside ref element + if (startedInside || !startedWhenMounted) return; + // Do nothing if clicking ref's element or descendent elements + if (!ref.current || ref.current.contains(event.target as Node)) return; + + handler(event); + }; + + const validateEventStart = (event: MouseEvent | TouchEvent) => { + startedWhenMounted = ref.current !== null; + startedInside = ref.current !== null && ref.current.contains(event.target as Node); + }; + + document.addEventListener("mousedown", validateEventStart); + document.addEventListener("touchstart", validateEventStart); + document.addEventListener("click", listener); + + return () => { + document.removeEventListener("mousedown", validateEventStart); + document.removeEventListener("touchstart", validateEventStart); + document.removeEventListener("click", listener); + }; + }, [ref, handler]); +}; diff --git a/packages/surveys/src/lib/utils.ts b/packages/surveys/src/lib/utils.ts index 128583fe02..9167ca58fe 100644 --- a/packages/surveys/src/lib/utils.ts +++ b/packages/surveys/src/lib/utils.ts @@ -1,5 +1,4 @@ import { ApiResponse, ApiSuccessResponse } from "@/types/api"; -import { MutableRef, useEffect } from "preact/hooks"; import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers"; import { type ApiErrorResponse } from "@formbricks/types/errors"; import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js"; @@ -106,39 +105,12 @@ const getPossibleNextQuestions = (question: TSurveyQuestion): string[] => { return possibleDestinations; }; -// Improved version of https://usehooks.com/useOnClickOutside/ -export const useClickOutside = ( - ref: MutableRef, - handler: (event: MouseEvent | TouchEvent) => void -): void => { - useEffect(() => { - let startedInside = false; - let startedWhenMounted = false; +export const isFulfilled = (val: PromiseSettledResult): val is PromiseFulfilledResult => { + return val.status === "fulfilled"; +}; - const listener = (event: MouseEvent | TouchEvent) => { - // Do nothing if `mousedown` or `touchstart` started inside ref element - if (startedInside || !startedWhenMounted) return; - // Do nothing if clicking ref's element or descendent elements - if (!ref.current || ref.current.contains(event.target as Node)) return; - - handler(event); - }; - - const validateEventStart = (event: MouseEvent | TouchEvent) => { - startedWhenMounted = ref.current !== null; - startedInside = ref.current !== null && ref.current.contains(event.target as Node); - }; - - document.addEventListener("mousedown", validateEventStart); - document.addEventListener("touchstart", validateEventStart); - document.addEventListener("click", listener); - - return () => { - document.removeEventListener("mousedown", validateEventStart); - document.removeEventListener("touchstart", validateEventStart); - document.removeEventListener("click", listener); - }; - }, [ref, handler]); +export const isRejected = (val: PromiseSettledResult): val is PromiseRejectedResult => { + return val.status === "rejected"; }; export const makeRequest = async ( diff --git a/packages/surveys/src/lib/video-upload.ts b/packages/surveys/src/lib/video-upload.ts new file mode 100644 index 0000000000..36563ca74c --- /dev/null +++ b/packages/surveys/src/lib/video-upload.ts @@ -0,0 +1,127 @@ +export const checkForYoutubeUrl = (url: string): boolean => { + try { + const youtubeUrl = new URL(url); + + if (youtubeUrl.protocol !== "https:") return false; + + const youtubeDomains = [ + "www.youtube.com", + "www.youtu.be", + "www.youtube-nocookie.com", + "youtube.com", + "youtu.be", + "youtube-nocookie.com", + ]; + const hostname = youtubeUrl.hostname; + + return youtubeDomains.includes(hostname); + } catch (err) { + // invalid URL + return false; + } +}; + +export const checkForVimeoUrl = (url: string): boolean => { + try { + const vimeoUrl = new URL(url); + + if (vimeoUrl.protocol !== "https:") return false; + + const vimeoDomains = ["www.vimeo.com", "vimeo.com"]; + const hostname = vimeoUrl.hostname; + + return vimeoDomains.includes(hostname); + } catch (err) { + // invalid URL + return false; + } +}; + +export const checkForLoomUrl = (url: string): boolean => { + try { + const loomUrl = new URL(url); + + if (loomUrl.protocol !== "https:") return false; + + const loomDomains = ["www.loom.com", "loom.com"]; + const hostname = loomUrl.hostname; + + return loomDomains.includes(hostname); + } catch (err) { + // invalid URL + return false; + } +}; + +export const extractYoutubeId = (url: string): string | null => { + let id = ""; + + // Regular expressions for various YouTube URL formats + const regExpList = [ + /youtu\.be\/([a-zA-Z0-9_-]+)/, // youtu.be/ + /youtube\.com.*v=([a-zA-Z0-9_-]+)/, // youtube.com/watch?v= + /youtube\.com.*embed\/([a-zA-Z0-9_-]+)/, // youtube.com/embed/ + /youtube-nocookie\.com\/embed\/([a-zA-Z0-9_-]+)/, // youtube-nocookie.com/embed/ + ]; + + regExpList.some((regExp) => { + const match = url.match(regExp); + if (match && match[1]) { + id = match[1]; + return true; + } + return false; + }); + + return id || null; +}; + +const extractVimeoId = (url: string): string | null => { + const regExp = /vimeo\.com\/(\d+)/; + const match = url.match(regExp); + + if (match && match[1]) { + return match[1]; + } + return null; +}; + +const extractLoomId = (url: string): string | null => { + const regExp = /loom\.com\/share\/([a-zA-Z0-9]+)/; + const match = url.match(regExp); + + if (match && match[1]) { + return match[1]; + } + return null; +}; + +// Always convert a given URL into its embed form if supported. +export const convertToEmbedUrl = (url: string): string | undefined => { + // YouTube + if (checkForYoutubeUrl(url)) { + const videoId = extractYoutubeId(url); + if (videoId) { + return `https://www.youtube.com/embed/${videoId}`; + } + } + + // Vimeo + if (checkForVimeoUrl(url)) { + const videoId = extractVimeoId(url); + if (videoId) { + return `https://player.vimeo.com/video/${videoId}`; + } + } + + // Loom + if (checkForLoomUrl(url)) { + const videoId = extractLoomId(url); + if (videoId) { + return `https://www.loom.com/embed/${videoId}`; + } + } + + // If no supported platform found, return undefined + return undefined; +}; From 5cae0febc96c88b913ee891551961934ebf62973 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:28:13 +0530 Subject: [PATCH 004/411] fix: variables initialization in logic editor preview (#4819) Co-authored-by: pandeymangg --- .../{default => health}/health-check.mdx | 0 docs/api-reference/openapi.json | 2 +- .../surveys/src/components/general/survey.tsx | 20 ++++++++++--------- 3 files changed, 12 insertions(+), 10 deletions(-) rename docs/api-reference/{default => health}/health-check.mdx (100%) diff --git a/docs/api-reference/default/health-check.mdx b/docs/api-reference/health/health-check.mdx similarity index 100% rename from docs/api-reference/default/health-check.mdx rename to docs/api-reference/health/health-check.mdx diff --git a/docs/api-reference/openapi.json b/docs/api-reference/openapi.json index 0afce99688..379982f927 100644 --- a/docs/api-reference/openapi.json +++ b/docs/api-reference/openapi.json @@ -7725,7 +7725,7 @@ } }, "summary": "Health Check", - "tags": ["default"] + "tags": ["Health"] } } }, diff --git a/packages/surveys/src/components/general/survey.tsx b/packages/surveys/src/components/general/survey.tsx index 0ac7c4796b..044e149a3b 100644 --- a/packages/surveys/src/components/general/survey.tsx +++ b/packages/surveys/src/components/general/survey.tsx @@ -119,12 +119,22 @@ export function Survey({ }, [apiHost, environmentId, getSetIsError, getSetIsResponseSendingFinished, surveyState]); const [localSurvey, setlocalSurvey] = useState(survey); + const [currentVariables, setCurrentVariables] = useState({}); // Update localSurvey when the survey prop changes (it changes in case of survey editor) useEffect(() => { setlocalSurvey(survey); }, [survey]); + useEffect(() => { + setCurrentVariables( + survey.variables.reduce((acc, variable) => { + acc[variable.id] = variable.value; + return acc; + }, {}) + ); + }, [survey.variables]); + const autoFocusEnabled = autoFocus ?? window.self === window.top; const [questionId, setQuestionId] = useState(() => { @@ -146,12 +156,6 @@ export function Survey({ const [history, setHistory] = useState([]); const [responseData, setResponseData] = useState(hiddenFieldsRecord ?? {}); const [_variableStack, setVariableStack] = useState([]); - const [currentVariables, setCurrentVariables] = useState(() => { - return localSurvey.variables.reduce((acc, variable) => { - acc[variable.id] = variable.value; - return acc; - }, {}); - }); const [ttc, setTtc] = useState({}); const cardArrangement = useMemo(() => { @@ -162,9 +166,7 @@ export function Survey({ }, [localSurvey.type, styling.cardArrangement?.linkSurveys, styling.cardArrangement?.appSurveys]); const currentQuestionIndex = localSurvey.questions.findIndex((q) => q.id === questionId); - const currentQuestion = useMemo(() => { - return localSurvey.questions.find((q) => q.id === questionId); - }, [questionId, localSurvey.questions]); + const currentQuestion = localSurvey.questions[currentQuestionIndex]; const contentRef = useRef(null); const showProgressBar = !styling.hideProgressBar; From 884b6f12aec1e4ccb79ed89c6bc15ca177d4aa3f Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Tue, 4 Mar 2025 07:36:23 -0800 Subject: [PATCH 005/411] docs: update API intro and key management docs (#4841) --- docs/api-reference/generate-key.mdx | 44 +++++++++++++ docs/api-reference/introduction.mdx | 14 ---- docs/api-reference/rest-api.mdx | 88 +------------------------- docs/api-reference/test-key.mdx | 50 +++++++++++++++ docs/images/api-reference/config.webp | Bin 0 -> 43154 bytes docs/images/api-reference/label.webp | Bin 0 -> 19908 bytes docs/mint.json | 2 +- 7 files changed, 98 insertions(+), 100 deletions(-) create mode 100644 docs/api-reference/generate-key.mdx delete mode 100644 docs/api-reference/introduction.mdx create mode 100644 docs/api-reference/test-key.mdx create mode 100644 docs/images/api-reference/config.webp create mode 100644 docs/images/api-reference/label.webp diff --git a/docs/api-reference/generate-key.mdx b/docs/api-reference/generate-key.mdx new file mode 100644 index 0000000000..29d1b3b3e1 --- /dev/null +++ b/docs/api-reference/generate-key.mdx @@ -0,0 +1,44 @@ +--- +title: "Generate API Key" +icon: "key" +description: "Here is how you can generate an API key which gives you full access to the Formbricks Management API. Keep it safe!" +--- + + As of now, API keys are located in the Project Configuration page. We are moving them to the Organization Settings page in the upcoming release. For you, nothing will change. + + +## Generate API key + + + + Go to the Configuration page of your project: + ![Configuration Page](/images/api-reference/config.webp) + + + + Click on the **API Keys** tab. + + + + Decide if you want to generate a key for the development or production environment. If you want to switch environemnts you can do so in the top right corner. Click on the corresponding button. + + + + Add a label to your key to help you identify it. + ![Label key](/images/api-reference/label.webp) + + + + Copy the API key and save it in a secure location. You won't be able to see it again. + + Store API key safely! Anyone who has your API key has full control over your + account. For security reasons, you cannot view the API key again. + + + + +## Delete API key + +- On **Configuration** > **API Keys** page, find the key you wish to revoke and select “Delete”. + +- Your API key will stop working immediately. \ No newline at end of file diff --git a/docs/api-reference/introduction.mdx b/docs/api-reference/introduction.mdx deleted file mode 100644 index 854b65f0b0..0000000000 --- a/docs/api-reference/introduction.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: "API v1.0.0" -icon: "code-compare" ---- - -Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has *different* authentication requirements, and provides access to different data and settings. - -### API Key Setup - -Checkout the [API Key Setup](/api-reference/rest-api) to access the Management APIs with an API Key. - -If you’ve forked the collection and are running it, update the `apiKey` and `environmentId` in the collection variables with your values. We also provide post-run scripts to help auto-assign variables when running scripts. - -Need more help? Visit our [Website](https://formbricks.com/) or join our [Discord](https://formbricks.com/discord)! diff --git a/docs/api-reference/rest-api.mdx b/docs/api-reference/rest-api.mdx index 404099eca0..8adfd726e3 100644 --- a/docs/api-reference/rest-api.mdx +++ b/docs/api-reference/rest-api.mdx @@ -5,9 +5,8 @@ description: " Formbricks provides two APIs: the Public Client API for frontend survey interactions and the Management API for backend management tasks." --- - - View our [API Documentation](/api-reference) in more than 30 frameworks and languages. - +Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has *different* authentication requirements, and provides access to different data and settings. + ## Public Client API @@ -17,7 +16,7 @@ We currently have the following Client API methods exposed and below is their do - [Displays API](/api-reference/client-api->-display/create-display) - Mark a survey as displayed or link a display to a response for a person. -- [People API](/api-reference/client-api->-people/create-person) - Create & Update a Person (e.g., attributes, email, userId, etc.) +- [Contacts API](/api-reference/client-api->-contacts/update-contact-attributes) - Update contact attributes. - [Responses API](/api-reference/client-api->-response/create-response) - Create & Update a Response for a Survey. @@ -41,88 +40,7 @@ We currently have the following Management API methods exposed and below is thei - [Webhook API](/api-reference/management-api->-webhook/get-all-webhooks) - List, Create, and Delete Webhooks -## How to Generate an API key -API requests require a personal API key for authorization. This API key gives you the same rights as if you were logged in at Formbricks UI - **keep it private!** - -- Go to your settings on [Formbricks UI](https://app.formbricks.com). - -- Go to page “API keys” - -![API Keys](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738097810/image_jvhqsd.jpg) - -- Create a key for the development or production environment. - -- Copy the key immediately. You won’t be able to see it again. - -![API Key Label](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738098072/image_zjkvok.jpg) - - - **Store API key safely! Anyone who has your API key has full control over your - account. For security reasons, you cannot view the API key again.** - - -## Test your API Key - -Hit the below request to verify that you are authenticated with your API Key and the server is responding. - -### Get My Profile - -Get the project details and environment type of your account. - -### Mandatory Headers - -| Name | x-Api-Key | -| --------------- | ------------------------ | -| **Type** | string | -| **Description** | Your Formbricks API key. | - -### Request - -```bash cURL -GET - /api/v1/me - -curl --location \ -'https://app.formbricks.com/api/v1/me' \ ---header \ -'x-api-key: ' -``` - -### Response - - - - -```bash 200 (Success) -{ - "id": "cll2m30r70004mx0huqkitgqv", - "createdAt": "2023-08-08T18:04:59.922Z", - "updatedAt": "2023-08-08T18:04:59.922Z", - "type": "production", - "project": { - "id": "cll2m30r60003mx0hnemjfckr", - "name": "My Project" - }, - "appSetupCompleted": false, - "websiteSetupCompleted": false, -} -``` - -```bash 401 (Not Authenticated) -Not authenticated -``` - - - -### Delete a personal API key - -- Go to settings on [app.formbricks.com](https://app.formbricks.com/). - -- Go to the page “API keys”. - -- Find the key you wish to revoke and select “Delete”. - -- Your API key will stop working immediately. --- diff --git a/docs/api-reference/test-key.mdx b/docs/api-reference/test-key.mdx new file mode 100644 index 0000000000..a2f55d3ceb --- /dev/null +++ b/docs/api-reference/test-key.mdx @@ -0,0 +1,50 @@ +--- +title: "Test API Key" +icon: "message-check" +description: "Here is how you can test your API key to make sure it is working." +--- + +To test if your API key is working, you can use the following request: + +### Mandatory Headers + +| Name | x-Api-Key | +| --------------- | ------------------------ | +| **Type** | string | +| **Description** | Your Formbricks API key. | + +### Request + +```bash cURL +GET - /api/v1/me + +curl --location \ +'https://app.formbricks.com/api/v1/me' \ +--header \ +'x-api-key: ' +``` + +### Response + + + + +```bash 200 (Success) +{ + "id": "cll2m30r70004mx0huqkitgqv", + "createdAt": "2023-08-08T18:04:59.922Z", + "updatedAt": "2023-08-08T18:04:59.922Z", + "type": "production", + "project": { + "id": "cll2m30r60003mx0hnemjfckr", + "name": "My Project" + }, + "appSetupCompleted": false, + "websiteSetupCompleted": false, +} +``` + +```bash 401 (Not Authenticated) +Not authenticated +``` + diff --git a/docs/images/api-reference/config.webp b/docs/images/api-reference/config.webp new file mode 100644 index 0000000000000000000000000000000000000000..09f2aefb5f4bd3c2d83b712daa1156a1290a5f12 GIT binary patch literal 43154 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 Tl4D%m1UHP~CVuGPN2jgDc2l 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�JfMvn@=@!vqd+oJVcO(XA4OU&VbX{|%C!OY~ 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 Ycje)`L3zB(ao!CHLG(Z4e__D?09b-Rj{pDw literal 0 HcmV?d00001 diff --git a/docs/images/api-reference/label.webp b/docs/images/api-reference/label.webp new file mode 100644 index 0000000000000000000000000000000000000000..e41002841cab448cb52aae955e58009be0d6b212 GIT binary patch literal 19908 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 Date: Tue, 4 Mar 2025 21:32:54 +0530 Subject: [PATCH 006/411] docs: adds email followup docs (#4858) Co-authored-by: Johannes --- .../email-followups/followup-content.webp | Bin 0 -> 15058 bytes .../email-followups/followup-form.webp | Bin 0 -> 40124 bytes .../email-followups/followup-recipient.webp | Bin 0 -> 41278 bytes .../email-followups/followups-tab.webp | Bin 0 -> 23974 bytes docs/mint.json | 45 +++++------ .../general-features/email-followups.mdx | 74 ++++++++++++++++++ .../question-type/address.mdx | 0 .../question-type/consent.mdx | 0 .../question-type/contact-info.mdx | 0 .../question-type/date.mdx | 0 .../question-type/file-upload.mdx | 0 .../question-type/free-text.mdx | 0 .../question-type/matrix.mdx | 0 .../question-type/net-promoter-score.mdx | 0 .../question-type/ranking.mdx | 0 .../question-type/rating.mdx | 0 .../question-type/schedule-a-meeting.mdx | 0 .../question-type/select-multiple.mdx | 0 .../question-type/select-picture.mdx | 0 .../question-type/select-single.mdx | 0 .../question-type/statement-cta.mdx | 0 21 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 docs/images/xm-and-surveys/core-features/email-followups/followup-content.webp create mode 100644 docs/images/xm-and-surveys/core-features/email-followups/followup-form.webp create mode 100644 docs/images/xm-and-surveys/core-features/email-followups/followup-recipient.webp create mode 100644 docs/images/xm-and-surveys/core-features/email-followups/followups-tab.webp create mode 100644 docs/xm-and-surveys/surveys/general-features/email-followups.mdx rename docs/xm-and-surveys/{core-features => surveys}/question-type/address.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/consent.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/contact-info.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/date.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/file-upload.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/free-text.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/matrix.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/net-promoter-score.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/ranking.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/rating.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/schedule-a-meeting.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/select-multiple.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/select-picture.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/select-single.mdx (100%) rename docs/xm-and-surveys/{core-features => surveys}/question-type/statement-cta.mdx (100%) diff --git a/docs/images/xm-and-surveys/core-features/email-followups/followup-content.webp b/docs/images/xm-and-surveys/core-features/email-followups/followup-content.webp new file mode 100644 index 0000000000000000000000000000000000000000..a6f65b5d2393ac362b2e5cc011e6ce57852b379e GIT binary patch literal 15058 zcmZ`Px4?Pdcw@!0qzVg&@^aZiN@|cGO|1^(5H*L1 zk6sOrEN+ae4&F63e&b5Um|&*qjU?T4-|_YHd4C*vKD`ZJ;6H9je&N3~Uw*B9nRt76 zPrZ|QwC+kycgKE2Jn+5XFHd!U*?p9LLBGvB`9E6U>ZEp=zK49S&7+E>XzMXh~Jt4kj ze|>!!_g!5Ue_dAsQL)Hkz}Q2UM^pV#Q#-AXw1=!#!w0nfR3)2#^cbrvjN5E zL-6MV{FmN1Y!I?4ib}F@*CIwpGjMeShyQp?)58wQ@KuDI`g~KJ@metNQ_BJ#2}Pq~ zFywZvVf0t;bjk(V@hmXIM(p1gOI#2@o`klTP$6=bH-BGt+ZV%X4LF|WBG44%#j(Fq^*<2 z_~Agb^hIr1c*3`cj&CXW7`1zCS^5kL!w5#=J19W1-lZ$+%cyI}s`zhL|2_DpP30&Z zsst27$xa6{vc(Xdv$m9!-hW%!gr;bk;)S;Bt>3B7_C97N}`HIw{eUuR0 z^7Wci2!6A}N%#M%b5D`z2s=Q!WZg)JQU|#VJGE-is;gFZ)}V$nE43j7CK?I1w!@fKptiLAA-XL{I+1MHO{)*1+#O@y5ma@``l6lyh|tbYsY zf6@EjaLj7K%T6L~2Y$skN(`iqR^K=!QKt&kZ5yYz)_==)|8EKT*KQwe`Rh?CLZ2sA zFTo`;ty)`(MS#t9*!_P=FrlLQ6M>&UP=D6iZM*Z~JA1L}=7>zZ`x59?>F-X?p+`Z7 z^vxD}+a$f$)g8x!K8~Bba4MOa<;-1UbBvuy4%2p1g@D6QDmn zQ7*4}E z*x_cf-n1$THu%msW6l_#(FJ9`koo!mKI~iI$*JjM zVvgX)oYWXUT5LBNkxv5gU^4}zYA}CBebmJvgbI{V<+2+X+4?gF+6IOZ-84%sLBVft zMalyHB;O}w%t*79fT$6Wp7^~oCx1Wp{kT*bi9&kL1D80YsI`L`o+?(zy@9%ReQ9OQ zv;azF=K0eAT+B2!p3l*mz1M`rB5r71*vTs<^!3?S?KJsH9&fP7aOrjINoEBB4rSNT zF5aL7PL!8KyEcQkCwgWj_gc8!9tCiRupUJkuZjB^4HMDko?GUDoHhz_bu(-7{ZsT9Eld6>#$~`@nWHxnZcLi*R{Zc_+8TGL$lnli>*O-XbMeV6w>=Y8#h2UuGdl{l z=i{946;yCY`N2$k6dlk&dk&rP^H!f5(QpJhHbD{xEC3|5TKEGxSUX(*IkF33k$;?> zXv96dq}S>FXEn9EVGWo4|5jU0eOkrATK~^@0GD=;Do)?M*B4* zeXhdVfL!9AV-O*y^pz%*nLhZ5?6tbRbAC1mfa}x2iW_af&aL{GAGrHSbg$P!jJ)hK z;NKb=%K(7SuhAF)007k8dF7jHaFm(Ql>u1U2=mIy5yWkfaT zOnJd|&CcR0)7Zxc+0Ix7GWj8CM~sozGaB})_U)kEsgl$r0U$EB5+}NLVe9kfzH6z+ zsc;qh-%*T`UYV{0hNct`${b{Yt8Z}T`Ds8x04L(Y{8{mKqfpmgaA` zjHAzhkDK0Xl3;!d?v#jGFInNz6nh@Ew`@iABF}c5jn6s3yWn>6<-Tsh0P|q7e7$L( zVhE2Yp_B-{=xqXP8`9{7ERPDks2aSg^hV?`#>FsWS4=#N&2wFoUlH=Amq>gsvhOzt zCD0fZ?462KXP^ayskwOAkV1R^B1_LOt`dqS`-N_{)jMAxnHnqSt_RSNar(;}%M=7Z;fCW{Mo3K?O6$TJB8-6pZ@;kLznen>u3Gv%8 zG%u-rag#_^OiULg@`*T0T7({vT+kPN(LH1poGJ*xGtvhgG)qoot6^FEa|)13pWP{~ zC#!;vn1ce20~jCZjYIo#aJ`JK0^U>adN)~@T+Z=5rt4=H{sHR{`_tu_eGdASvv<{7`YeZXuEo5VBDWosb3jK#P#D`JZQZZYknhBz27g^E6 zHLyf_0Tq8IsQYA)y|U<4X?v)78}R~l&fd*QhXq~7!8Mi|uInzIhODl%2u=(h7MEkE zr#n(hGfvdw(eOovHE?zvi#TfjDfpxsY`kG%%93a0-&!S0y1!b!CV+#jFhVaHXh$Va zM&4F`4o~x$G`(Jj+@9EUk|Io8NF z$84d6p-$=7c-M`vFhaTCG~HvR-hHnLq;AN`!0aLv!7=xCsx=21pNiTcW|-lP|Hsd= z+Zt@7z$Ui|mGfUU$?baTvO^xFp$7NkFbExpO+4&{)ynRLKknit5xkF#a{8R`Yc7*I ztD6dLYy_vp?-M)?zUbk#!a>U#U8R%FLMIOk|dPw88VhT4qE4Zy9>;h zF`@uFXAjYFe2aK#Y)N7he0Bu4GpR?Sk-%Q%>1?A8!yiBU`|@p_UI1$9##W$wPoU>= ztpBAeCFQXcxIb5&W_A;4bP%oIE_t-wm)}Y%LC}*Ak`Ov_38xx%{E`G%>-E<4$7v@j z#WP3_&U8zplils519GY%?|vr%!PUqOFerYIngm(*z+&QM2M`%}dvMq@T99cr$Ubg60C3%G9-;*(irWeRv}siu3*W-KG^*^MwS z!tK?_8TT|){F*nO*DxvwsH1lByvj85s@R#zh2If>jd+E=b0L97TWo-^s}pE=uf2yW zaK&d9^0A}_%r_W4X-)0+luqn3a^o_DXh@nOZNvmTr-?Dp>Cgt2$Y!2amA2u^|A;H^jM8VLe~sUaH6To^5T7s z9zpsGb>`|TCfF2Rw;Vn^P9VA?>mf7=9)(poz4N3F#WCU}nMw}08vp=acXC0=9!aV; z$Z>I`Ws>0Gaut?=hmN}Q5XW$#hgH4Z2J*g}lT*E3Yy3pbg6v~DmR5FgvXl>#J`4IC z+P(e4hgyxZP@klR=nB+R_7?285Dk_oT8A>Qj6oEg{;h(cqvwbRiLbs%?%#e{q|N1r<2W$;+@pH4G1`qJ}Roa2ELnd)0!z7{plO1{!wC5f$ z@ZJHLX)4N{nhO0@+Sp*$gd9IGqNB{=7Jt6CE7M2hPg>_Jtrj3{K7Pton1dTHj-PGm z27~6v=qI+>+hIS##`~s(1TWn6MxeM!<(k@r*YU*11Cm6c%wYYC#Ui7VlkX_3gI}@L7DJjRy$ZC}W#UyccM|C2{y`LErvxR5J8VWzWbvBuXM|{aI!r-hEPRD9 z$ektP^M>i6OgQ;2AXAA!16VQ>* zD(}TfG;pA^>ft#5trzfSoNi%>hzTlm_xxd_5Vi&+6qAHlEB*SkR7#!n;!k{L68i)AJs=Nml}c}S-6A;;vC zG6rEs$$;=rmOQq?RJ0nLgfk@5j{;)6`I!>fQ+GfmTH+>wGdVW}3H=JhQbc1$QO}#K zZF+Mvg4a-qgKx~U;&KHPB4JyTO6m2r#gXj&n(Df;Y&ItDcn0!Vy7Z+lMvg^uim-(Q zkq#!juz?r_{U9?0A0*i-?E!K%qs;^5nDhevH{I=;wW%dfMo|H^g$8$((i<0^ol1)! z`Bo3JDc4$Pr*-LNb2j>40&(?wGqS6tmzUsc4;C6ZDB3}EtzP5);=E6lGO<5+BERu% zDP8+dMD!JBsnc6KCw-3<3cu%W`;2c$W;x#=A}BO7DarXEdfM`{q;kQ|Q}>6!=Z{np zIzf`fhaU4vCeKHw+}0*uB5^+kvw<6}plGH0L|*kG=@&-dCMnnB0_ZPd$~!Kh0FFGm zyW2YD^k2}vmsSNEaw~$8`W<*r-gwl6s6=&+%1cY3Yxu39roTU*;<)HHQS4JGW5r@=eTe*6kLg&?1V^l+&HB`3OKIeYC{Pq4#b_c$VQ+Y^M;AG?8QJzIpsmDGmDk% z%Eq15H{MJzVcaFpsI5>JcPsLPkAnU1Lk1{zQu=xo`)W-Z#xa|InPcuZIdOl)7o8vI zrlVZrY;Cr>t&x$pikl|1&Rah=tNJ4aVhpAe!jWkq4z6CB8??n_Df&sNT3b0HF^Wv06E`>cDg=E-#a*W>oU}@Cg z1@ccjuPXil<>upS0}95_?46o{(yLyM90C`S-&mY%&)-IV+qrpsE-Z|2m@xtk>^K(I zo`r2$`fyD~xr35u=*%#q^ilp8M34VaIzoEgx$QH`(QCf{aW6QT02V&CAAOPrXau>W z7ECi7<7|fIvlVski^vw7+WS5TI%5h%Y3TNSUe}Lg@;H7mFOa!JZ)>sP;@aqpvE2ES z_49W|b$-osVFQ>BO1_BAXp@}0N0PMf7lqIi?Esc?jK47;23U(qtEUx%Bz_vpYGMLl zdS*%I#CWz45J@fMMZ;@j?v(~q=6g-Y3yd6qh%~43^{sE#Vqe z-7z|;SH^EYTGuG0$Kb12kLkW6Cu?XT0*G8rXl{>Cp}z2Ito5a2!t9nO&{nUW!aims zX}0D~6ojn(qUbZ`UcCT+nZO9}>3FI~YrS`p0Y2{Lj)BwzFjX6uFH#>+-^A1E2a*?H zD!GP7010R^VzQ9?JzEpVHuJzP+l$S)dsz==N%ua)mu4}msXrjh71U4Y(rwOXEa3zJ zk+rpdjB9WwNf=7N$*eY!fVk-9aQGH2K~Dkm&96twBz0>ojcmE@W6>d)H#Tpo1J1ru+R<8Qda=R&d*k4;~@kPl-3u$U{h`lzi zoUMFSETK17J^Kj+)nUlcdu#}Mp2rWmo7A-+D8fODH+v))%X=GekNl4 zQL+*wP7%LxW``e_vXIUpIJPlukdLz}LCq1pq^`?0fUY~2kA4}zJIBCBME{`>4Pv?ykbmor0BOW6>88QwscjZGC!aRda0&MzFwZ zTRya@(!)3$icUp$DSIS@Om78%ihT&`xELI7Hv{bUlOuw}z{dbtk~B@48cUt2!HkYe zbLNqA*no~b`feD8b2hD-k=eoknER)~I>GI8*l94q>f|K=_=^Ty?=|S(Hh9c>@1CYO3LZ@?)S7Y>uIJ`K>;QtdVmVXtW#zY_v?A6vG&pgK zXD+^`?hCRwH5j7garxT`{Z>)McElb;ifG!p!9D?ZQDKhYH7c{6LoCs3iJ7LO{#H)p8Q; z1YPai?t=CjhoZ&uv2+E~jxi&bwgo&iQ#7(U;;FBDuINczMu{VrT!zrr=o~|Vu#x7~ zlK#{eZTj^^SJCvcczxZ(N#_-ydeHvJ8N0I@RZA0pdnWz-6d;pb|thA!EJg&R6Bd{j>%Q$(fct( zkhbQjOzWO06zR7ih!jf7Oh|kH0Q5qh!?yYhm7`Z-Ak513J4boNeZAG7kp0 z%F5PbNaJRp%opYzA4f~eslSw3Fjv2`VSvlct=zt;UGx}xNj>Z0YG~L`BTZQWbKM&E z15Q`hvi?$_VQ$6qVu1f#T)zjYU-OxKPrdHpY-&2nZna5Y+Z?_%6@uX+2D()sI#mXv z^aVtPHiHrTMPs+!Q0yyGM=q6dJM!r|95?PA1;barGO1{@lHw zH=@L-_ta>)08WuQbO==#4`q_vf%l5(t?2z2U`sBp1v=*%@0Lu-_BsAioBwbVkaCr3 zeyyEiZ(Yp4E-Bc{D|F*8fLTBWm~BDC?f}A}S|g+SpQ2+ygI$cZwg#~GqKD#q03cVf zc+Is%T^!@1uQ3=9C^Js+Gtt1St z19F$%PJbd1s#{fK{Tm`J_XoEUw!5-}PWjrT6VBTb11VMy0C1$T{wD==>O=m#LzNgZ zG}mQ`90ZiuZ0%XLqQV$D)t&J!hc3_u;`Cn#N=v&!%6*MQU&0}qBk*v{;VROqVm@x1 z1%N6okP$MicTJZ>-UGLbUJ}`XhG_~4gaiame9?WRDc%b6ym38=Nv)+G0nt3OVeSuA zW1tDaWQU5KK3y_d|60`QktFy7-~!faRfb)rnvaV}jai_H@V8Ea*sM?v?aWhgss><( zvsFiZp9DB0iMB0R`^=TXSOz{nj%ScRL417DRrR=#&>CHEbwX-5h|Y#O)RI+TV?<|@ z^ZXideSCb%9r?HJ!kFnIygq_eZ@O{Wn&?+&@|W#{$bnRq4NqynhqPNKNI)UDc+}k@ zWEnn%bj8+=je&dMH`5NVBO-#lA#llCKlpY)5?keQt~$N+N@OTt*Oy43$q%Q0-EZ1@ z(iZCTh6EvfrF<^3%f(X2pKLsWNiIO`&ZD0kg;>msN5uJFkBu`A4YR12j-N%u4P1(3 z3ZG{18hI0;3gyZ$#QgsAAiPGR(Ho2!XF^18c!J%y(SU+5TVh+tO8uBU2n7=>9vH&B z2DJaPdL@WjYju&CbbIw}1+?Wo=UgB_8zGoaj8z4-w}W^(=4IZ^PW({Ce{t;Sk$kWi z*~>eFHuiWq86Ukd{WK~V%_5c0wK39IM z*@Odpv_Q_aQnhZZ8|}#`e#n*Mal8z%6j(7<{@5!cfrC`8DiffVak{H*)bBG+W*(g4 z{bwHYlDmW0c9;bm9zR#_@agls9i}lx7}pZcBR%TS9U~rESjHCUW!flf?yA$1q`2A}On^IBGm=~pL8xwr0 zx~dEYSQHGneJuF6C^N}vgzS@DZG|lCh&OSt#CC1AYmnML(RnHvRX|7J*hNyaQ2Iu+ z15Uo82U4F#z65@-0|3V?How!-F_GC7O;=X8o2WpGUw$kqC<6bDQGPOmoPd9Vb1ogZ z1cnl%%kBdwxs^R}zxm07G;Tmjb2GPB9`wjY(pi^FNR8)H2(y!J>l@AVHu&9VQJQV# zc@oi;c7{HvbgnXy^Z@kfSZ-~fLKe;D17)7^*{MM0V2G$~Rpa zxGDjwNqnv6NoEw0Zq2F3fvnK4ZHLzhZbxU;fYWmMlMIz_7p9R^oKX0haW$XimQXrJ(X|-Jm-5S)^xnLZI5k2I-eb_0<7syWXcxi; zZ591-e`@9O-JHx~f~2_StDi0;Bjo?WjZ%@ef&hm$s{?IJsO_dztF+#YC!oxGL+2Cv zzVWC4D#d?XYn%{I0pfEy)OTnQrED9q{B?EVN#4wGOr7W$)cSddB)7W~V6bkxJj#8n zy^6JWLjyA-zG_eYX`!@|EY*QKJ!&sUs)W z@+f9)3uI-_t;AShbDj7C@WTIuFVhGct@m5c5H%X7wuP=tUh-E!hp(%T?(tkplk|m5 z3$9T$-lU63{IF^Wv=!2%M6NFb(#!VpNQ;c0|ZU9nMO@4-rL8TE?kb_HhK>^GFaRV2N4( z*U;Cc)~9&H)iDmI3#;=X6lcy!kcZ3z2nd*~aEHzLNxP1m>Jvg&I5c``g^hT$@>dxp zXVzlN^!+!ib8;KJ7`^kwi`?ktA2Zp+?mQUUq>Rd__*lYKo?xRW7pEZ->X@KE3UBwd zMJZ@#oX+#UA8+z$H#>`9Kh>_UhfrzSen905aolCc)@S028G_k*MZQ{(>kS&ddjElF)xZzF!iQ*#Pj~dRx{S{n_t}pW~m4I zHQOIMDyq2a+rMt&yz)$W&}yHT+EmYcSN^3d$6nqu`ZEs3{|S+PZgQ^J9axKRhBIxf z4txl`D)Sdk1Grmk2y}FZ;rQ-HwrF0_-3j$7Flu$ z0&gK=y|NUP-=m*W2^c3kSk!2IkSo@ApnRHD^YXcpNN9LmS5B}Ms!2MZXUcf}EPk`A zEMCyvbNR6Dt&j5#HE5-Jb5Ofypa+p$!~~+n_#aNS?)H-tq~3n zr}a+rIjahya8()xQRyb=)f-lcKAaD^4xcM|a4r)118sRu<-$C_o1c60i>Zl=$o9hD z%knps*War5y{p{|W*B>x+WEff_ipWlNK{in-lSJ{Tv$}`B{hBf;`_e#EV!z+Mh7s^ zwue|hi=A`SCth#`zJBVDJOb7%i6x3n(EVogR_8=EfmO$oY+<=g6x%<3@!jZN&S@Xt zO4-3AOD2y0Ge~q$nSnAs@jaD-ajuVCM%8-2+@@j&USld1kr139&WFsG_1$l>DEIqV z$Yo}iU0!Psv@QmBZ2&8nr>PIrFrK0FnZ2hkIcF_C&BXCx?u;mmLAHPi4mT32h2Gq^ zOlW7aiY7boXgGF)ro63Mj=4Xt`t_}MU8r{`jWq(?kdP(98+p>|dbS-{?XJeo+>KA* zHD0W|TK4Fo10N?_^BAPYhHt-d?JQ4FiuEHhgb1NjEQK@`Hsn6za~t{XX%W2{WX}WU z&}c5Qj(S4V3&vO;V%H7~X@~}*iaF8bR+{F%-|6p(^aa<7L7%b~3ub!T2e z|Js;h8-yk|hO`@%JEsDys7IP~~$K#KTCn#$IcSjBO&oTW*kx z?8wQsz5p^#xkXuPu`Bw#*GnM_-B%s|)Z0*GbUD(mjU`rb4x@0Y%QZuX_AWYiid3}$ z2H8^VSpKotb_LSx>rtci5W|_Fe8brJT4zXMdspA(Sl%i*GH?;qv%yLke_0$ zO$nWm8v`&`KXaO-e}BLPTM`Xq^@U$81YeFbs0{+!JSVBU(+)iz!#)XlCD)*Eg}&3EaF2wC#Y=Mm&0QBX&yN|szef~ zxW+Uou_)-USL@y6ivUY4&kHgzg^j22-mCC$5M!ixsgFgTpY7Q=`R+){ zG_{dCBPPBHlLhfGAt0T%Ee`ek@hHNylGc!56YqM&umJq(2*pYX_eFQZkmmZ^b!H!d zHdYVv?|ZQL$x+XIXWC6W@U6wq7E2rLM=@cA1gVKkL zq1^bl(Ca^)FNkjE@tqi$ZO5}LLC)6{Cw$w--z`vKD@9pMa=@zSFR5Z_PwkY~Wf}#5yMY6HSLlq^Kly-b z-a+yA%a>VoSa%YS10~GJF~`r}dQJrjQRh(gt(9_O1uqd=@(SM*ca?*)+Y$AKiuVuE zxl9&sC%RWAtx)z_wq;K~67Ba9AHftJVMq1Y`Hqx;Df##25 zt|HLa&J$osV~=Iyq#DdQ42^KW-S3$!?iUXqSRbOdqL4-{CG9Mm?o21t=2NG6eg1X^ z?a|YtGnu)i(Ijw`qnQ~bBdoawPir*;cOHE^9u2?59^UNvn3zW%;Xs8RvL-1o64m<5XL5<5Snj`exKZY|B`bp$hGj{J&VRrQkI%R_1)$>gmpxCV>_7D zmQg>i0Y1N=jUYd!W?Q%=3vjU@4O(G*^!R-2t2eY#sQt3y<0p#op2k49ntzRt_9ziT5m+* zmf;a6tJ27pPm~i_hGn54z8Revp&H&Q{%Vaoq(>I^x~91RU|kKCjg`&B(F4grP$ph! zA}$|zI6g6Or6I0Wd)Iy5`&U_P_2b~mb3DV5dmekzQhgUx%bI14wh`LWU;ux-ffBPu@60=oQ1-gZY)T1?q4y(d zfMPs`vHCiEKwRMdZ5k}O3g4KYx^7AcK8ScsSw?hBmAbN1{VZ{t-NjIZ)LXi!wZ(u$ zLxU20x>HOr4H~=pD_=e6%MTZAP?JZyW>j;`N(|>}aozY-Z%2ow zk?QPDgZ@0{b<>dwg(K3m>ZovpqzlnpUT=lUV1njw#fi+z`SF*JIaQ=Dfm5zwAH-s@ z=$>Yo)!CG@q*~keb5!+#TgkAA(Ulf%)(4KIs;T%Urbo3Lnc|nn2u-2~k2Tc`qk!jTUzF9^&+PEaD+7R0rT&4{-ip+AD>Jr|2 zB1qLz)CmPy zn6um6xeds>N@YKC4yySD#CoN!gjjE>a$LHm9}`IZ*xr=e*Kij@gTY%|F^GAlfG5zz z+8?_Nj#n8G0{%c?*k?Ll;J}?Rj&10%jtZ-~v2&um7n1j*l)vf5p_cqJ_p`A>Ml&_M zK?qvEt?I9cns=3FTFz>|o6B1yo)1|-av@!F7SzFw2wQj`m#5N{OVeQaihly1WN;R` zGPNB}3Xes`aOfX$TAW^yfUZE7-JPiXa)V7nGw9Q+DIjH=4={;3$&2fi131rExmUb; zDL(7eR?)YE3ZuG_2;D^w@`dR3z%w`jmV+TeBu{UDM|8Lk#+76rus@!z3>v>nf*VK2 z9>UAFSc+Q9>?*vEv$tBdHwuqvRcyg>NzTr>0BN7{|Hwoi$=*Mz&3igl6QBK+>4uif zf}}fEk-Ij=oy0Z!y{{?<2V^?aKOxq2_LkSY14Vfid}umPZ7i=fZmw3(RlGl7TVEga zep)VtzVebB=hySQEsmNl2Hvb8o_yaHcy(v`HhsO1YgwpYdd!7C z8_)2f+vrItZGXZ=3e8XbAlUF>5JP$vTR6K~uP2$Cul@p77HqKNwuffSwe1hvW4r(WZj}>bS>YG|I$`s@CYSm^Z>uK%ywXk7oUBno7oh z1Sm$Q??!f^OCaVJ*~pc?kS6f3ggjP0solb@zrz|iGOK%D!Q9$V_taGrE-V$2H-@8z zmOW9uMV$l0Rg5yG&!~O0E1})CI-uBzXsikbHQpC+&VwF@c0h;NI1jxXg1)dVM@sam8rm1WXT>lE<lpNcLlny_|Mn7id|6dv&&_bNsXNo^o*0&@?c%Xvv?+ENTgI*4SI=BU zov+%I`ku6#0<1mtvc2ACsxHiLLxVaWms%SA3Z2}J(E?R)Dri&apdy@4T)y#AJQZ0K z0xeJQju<*UWwrsF3a4(P$@T!_-1v|@Bfm_F`FsMargDEnF2}DmmN}TBd zHd2!>3vqy7Nt%dDM8@ z>d`0I>G9@|ZVccHVtl~=c{CBOnX8I8|FxK0jb+5=Qz<$ej~lk@Yr_`lNC-}tuDsqW}AxJajUy~)+KBLK_vNLL^#y|Iv98U*at#eJ9VvF*jXS;C=u z83wU16^a}6ihI8GUEH?DJv11mLsYVwku4n1Fj=i5+?7n1X$dtm#&P`#T7+j#cM~kP zWyKO6==$!839RR85=$emw=pqdyJw&r4soWZkPZ`4;t^F;Cf4sb=Wp+e5jK)I)r|M6d^u?sT1Fd%!qnX9&v!Tng~= zrTH2?r`2_&khU|e>WUoVa`fWPJyPvQ!)Td%aAYlCn$PeOsp=x#t1knGTh(g~bC8C_ zD!1kP%6?ydvJkIwI|@y(fO!_-uSC!g@KmTp9gcxRBP<*CD@ zmCwR#IfI(1`EXKWZgB{1<|S@Td_&7YX@O0W3O$s>Vy48zvUWN`yw zhBk{^1*df>*FK?=%2GGO9i{r4)L%{JfmZ`F@fPVvTOaMkt%}!aUC8Zy1mR~@a2KP=AUcOE3B@Zfts`f8lO-F1=c1u``hdQ)e1rP>#x zg8K9SI?&16Up_*7@^1gbr->LCc}5qcOH{SDk`p=uChV#BfV@4QYNUYDIBg28RB9#B z(chBQNmYR;9AhsyE>(_czciHwZPGy5@2Qv>HjzI2BHtlpD8<&Du_>nMcu}mkf7t%u z-D_D9^|igu_x`3BCjW#xG|B=U9f*v2qv=7h)swKB;5lqt7x`^(NAwe< z7$yIbD>A_Z9UqGQnv|mu-*z3^cBHYm#}USQVcnlYx61d+IC#CW(AJrYfdG4HwBK8* g*%@!lP_M+8PEv@TcGcC3hRF)^k|OOM@W0Fd0KBLvRR910 literal 0 HcmV?d00001 diff --git a/docs/images/xm-and-surveys/core-features/email-followups/followup-form.webp b/docs/images/xm-and-surveys/core-features/email-followups/followup-form.webp new file mode 100644 index 0000000000000000000000000000000000000000..0c1978b5a0b8dcda7c53a887ec860b8819f9db00 GIT binary patch literal 40124 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 SWrlk9_w6-uNZb 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

+&We7 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 literal 0 HcmV?d00001 diff --git a/docs/images/xm-and-surveys/core-features/email-followups/followups-tab.webp b/docs/images/xm-and-surveys/core-features/email-followups/followups-tab.webp new file mode 100644 index 0000000000000000000000000000000000000000..6459a8e66ce95d59507ede506d61a4a4d6fbe7c5 GIT binary patch literal 23974 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

*CQ`{fG```OPYi->HUjubTDan z1S(|n6;!V3xvLO3>hgUFGc8s!%pxr|zLOdEsEmRwHkDm~`!=Cc`C!UKsF*j2RB?$2GZG4CBbcRTyQ)|L$#wvHGZ=lJreoSM=t`5q5 z`dKG&%*o1MCP_b5M~BWYEOvBbt`7Yzq5J$e!{WS@U&IKMy5epgz7!pX{{wC zhSyvY>2Ja%+T0A?reTKAufCTILofALCPrjINDN|h$vEX4J;T@TB z(btQARhmH$jI`FLe@Tx-P8ezlsYbrUn%ekZ7B-wAh9Cr?RGk*-N>|2t<2~a84Hf@L*c!F*Y9tIFF$`|evNK>D*s7;{Rb9*um&mA4U50WslX)WAtw5aa9Po19{s z+UBDA^I8V@3V*%k*!^fM;u-yh1Wf_EydIVcLBEshOKFD4c(RnY5S7q?oP&e^%wfeI zjZ@DXo?naPge6$vTB5goMBt3EloTf@3UV!@r1$^q%&?w~#oqMIB)btmB0Q71-h`>| z(Y`l6$iwq_g~LKoMJRE8SR}|Jt9^Yo{yzvtt)3+8=&{I#i>iJ7u%;r2V!_CMNc~+il**5a6goZL>DycwEtO8=n8&(fzQ@ zkbb28M+#--J-r=VghUh`??Oyko)>>8v72l>!B4x`IA1dn+;p5|pIe5M?0wo?d+O*XM7v7G?{tpT>+)fG3YypVn8zBxxHjI6G(U#+|G z2XSSqz@O!zU%uD;VoJe^E4Vlvqo$RhHv_prq2bx2Mz2SrO}NYyJodO~hj z8`28TzhU{_?Y5xdKzL!+(v*_=d^-SlEGl~I9|IKsH-SOx(jm%S=Y}2m!n!9=P~qd} ze6Yp`i+0K7yDek8Qo$Q;_fR=s`ds|VYGl=dECkRUQHB!V1-bRC2N)xJtJ=kAhN7c| z!V+^)mz)+#1NrQu%3`WQafYk%+-X2q5ts8h!*n=69d+7+mC4--kUW521o}kV%&a~Z e^iTM3FV7W6nKJ!Y0ceXz*X{bCe**gdtN#Mzf|s2D literal 0 HcmV?d00001 diff --git a/docs/mint.json b/docs/mint.json index 57062bf1d5..4b22903445 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -48,7 +48,8 @@ "xm-and-surveys/surveys/general-features/schedule-start-end-dates", "xm-and-surveys/surveys/general-features/metadata", "xm-and-surveys/surveys/general-features/variables", - "xm-and-surveys/surveys/general-features/hide-back-button" + "xm-and-surveys/surveys/general-features/hide-back-button", + "xm-and-surveys/surveys/general-features/email-followups" ] }, { @@ -90,33 +91,33 @@ ] } ] + }, + { + "group": "Question Types", + "icon": "question", + "pages": [ + "xm-and-surveys/surveys/question-type/address", + "xm-and-surveys/surveys/question-type/consent", + "xm-and-surveys/surveys/question-type/contact-info", + "xm-and-surveys/surveys/question-type/date", + "xm-and-surveys/surveys/question-type/file-upload", + "xm-and-surveys/surveys/question-type/free-text", + "xm-and-surveys/surveys/question-type/matrix", + "xm-and-surveys/surveys/question-type/net-promoter-score", + "xm-and-surveys/surveys/question-type/ranking", + "xm-and-surveys/surveys/question-type/rating", + "xm-and-surveys/surveys/question-type/schedule-a-meeting", + "xm-and-surveys/surveys/question-type/select-multiple", + "xm-and-surveys/surveys/question-type/select-picture", + "xm-and-surveys/surveys/question-type/select-single", + "xm-and-surveys/surveys/question-type/statement-cta" + ] } ] }, { "group": "Core Features", "pages": [ - { - "group": "Question Types", - "icon": "question", - "pages": [ - "xm-and-surveys/core-features/question-type/address", - "xm-and-surveys/core-features/question-type/consent", - "xm-and-surveys/core-features/question-type/contact-info", - "xm-and-surveys/core-features/question-type/date", - "xm-and-surveys/core-features/question-type/file-upload", - "xm-and-surveys/core-features/question-type/free-text", - "xm-and-surveys/core-features/question-type/matrix", - "xm-and-surveys/core-features/question-type/net-promoter-score", - "xm-and-surveys/core-features/question-type/ranking", - "xm-and-surveys/core-features/question-type/rating", - "xm-and-surveys/core-features/question-type/schedule-a-meeting", - "xm-and-surveys/core-features/question-type/select-multiple", - "xm-and-surveys/core-features/question-type/select-picture", - "xm-and-surveys/core-features/question-type/select-single", - "xm-and-surveys/core-features/question-type/statement-cta" - ] - }, { "group": "Integrations", "icon": "bridge", diff --git a/docs/xm-and-surveys/surveys/general-features/email-followups.mdx b/docs/xm-and-surveys/surveys/general-features/email-followups.mdx new file mode 100644 index 0000000000..6d124c8eae --- /dev/null +++ b/docs/xm-and-surveys/surveys/general-features/email-followups.mdx @@ -0,0 +1,74 @@ +--- +title: "Email Follow-ups" +description: "Follow-ups are a feature that allows you to send emails to your users on different survey events." +icon: "envelope" +--- + +## Overview + +The email followup feature allows survey creators to automatically send customized emails to respondents based on their survey responses or when they reach specific survey endings. This feature is particularly useful for following up with respondents, sending thank you notes, or providing additional information. + + + Email followups is a paid feature. It is only available for users on paid plans or have an enterprise license. + + +## Key Components + +### 1. Trigger Types + +There are two types of triggers for email followups: + +- **Response-based**: Triggered when a response is submitted +- **Ending-based**: Triggered when respondents reach specific survey endings + +### 2. Email Configuration + +Each followup email can be configured with: + +- **Name**: A descriptive name for the followup +- **To**: Email recipient (sourced from): + - Open text questions with email input type + - Contact info questions + - Hidden fields +- **Reply-To**: One or more email addresses for replies +- **Subject**: Email subject line +- **Body**: HTML-formatted email content + +## Setup Process + +1. Navigate to the survey editor +2. Access the `follow-ups` section + +![Followups tab](/images/xm-and-surveys/core-features/email-followups/followups-tab.webp) + +3. Click the "New follow-up" button to add a new followup +4. Fill in the required information: + + - Followup name + - Trigger type (response or endings) + +![Followup form](/images/xm-and-surveys/core-features/email-followups/followup-form.webp) + +5. **Configuring Recipients**: + The "To" field can be configured to use: + + - Responses from email-type open text questions + - Responses from contact info questions + - Values from hidden fields + +6. **Configure the Reply-To**: + + - Add one or more valid email addresses + - Addresses can be added by typing and pressing space or comma + - Invalid email addresses are automatically rejected + +![Followup recipient](/images/xm-and-surveys/core-features/email-followups/followup-recipient.webp) + +7. **Configuring the Email Content**: + + - Subject + - Body: Supports basic HTML formatting (p, span, b, strong, i, em, a, br tags) + +![Followup content](/images/xm-and-surveys/core-features/email-followups/followup-content.webp) + +8. **Save and Activate** diff --git a/docs/xm-and-surveys/core-features/question-type/address.mdx b/docs/xm-and-surveys/surveys/question-type/address.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/address.mdx rename to docs/xm-and-surveys/surveys/question-type/address.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/consent.mdx b/docs/xm-and-surveys/surveys/question-type/consent.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/consent.mdx rename to docs/xm-and-surveys/surveys/question-type/consent.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/contact-info.mdx b/docs/xm-and-surveys/surveys/question-type/contact-info.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/contact-info.mdx rename to docs/xm-and-surveys/surveys/question-type/contact-info.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/date.mdx b/docs/xm-and-surveys/surveys/question-type/date.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/date.mdx rename to docs/xm-and-surveys/surveys/question-type/date.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/file-upload.mdx b/docs/xm-and-surveys/surveys/question-type/file-upload.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/file-upload.mdx rename to docs/xm-and-surveys/surveys/question-type/file-upload.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/free-text.mdx b/docs/xm-and-surveys/surveys/question-type/free-text.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/free-text.mdx rename to docs/xm-and-surveys/surveys/question-type/free-text.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/matrix.mdx b/docs/xm-and-surveys/surveys/question-type/matrix.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/matrix.mdx rename to docs/xm-and-surveys/surveys/question-type/matrix.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/net-promoter-score.mdx b/docs/xm-and-surveys/surveys/question-type/net-promoter-score.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/net-promoter-score.mdx rename to docs/xm-and-surveys/surveys/question-type/net-promoter-score.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/ranking.mdx b/docs/xm-and-surveys/surveys/question-type/ranking.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/ranking.mdx rename to docs/xm-and-surveys/surveys/question-type/ranking.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/rating.mdx b/docs/xm-and-surveys/surveys/question-type/rating.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/rating.mdx rename to docs/xm-and-surveys/surveys/question-type/rating.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/schedule-a-meeting.mdx b/docs/xm-and-surveys/surveys/question-type/schedule-a-meeting.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/schedule-a-meeting.mdx rename to docs/xm-and-surveys/surveys/question-type/schedule-a-meeting.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/select-multiple.mdx b/docs/xm-and-surveys/surveys/question-type/select-multiple.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/select-multiple.mdx rename to docs/xm-and-surveys/surveys/question-type/select-multiple.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/select-picture.mdx b/docs/xm-and-surveys/surveys/question-type/select-picture.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/select-picture.mdx rename to docs/xm-and-surveys/surveys/question-type/select-picture.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/select-single.mdx b/docs/xm-and-surveys/surveys/question-type/select-single.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/select-single.mdx rename to docs/xm-and-surveys/surveys/question-type/select-single.mdx diff --git a/docs/xm-and-surveys/core-features/question-type/statement-cta.mdx b/docs/xm-and-surveys/surveys/question-type/statement-cta.mdx similarity index 100% rename from docs/xm-and-surveys/core-features/question-type/statement-cta.mdx rename to docs/xm-and-surveys/surveys/question-type/statement-cta.mdx From a399fc7f80d7bd284d6534f37fd17381fd425c79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:37:59 +0100 Subject: [PATCH 007/411] chore: bump version to v3.3.1 (#4873) Co-authored-by: GitHub Actions --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 4b118195fb..b727741685 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/web", - "version": "3.3.0", + "version": "3.3.1", "private": true, "scripts": { "clean": "rimraf .turbo node_modules .next", From cdf687ad808261505f5692967d276dcd4ebf096a Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:10:00 +0530 Subject: [PATCH 008/411] fix: delete webhook button visibility (#4862) --- .../webhooks/components/webhook-settings-tab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/modules/integrations/webhooks/components/webhook-settings-tab.tsx b/apps/web/modules/integrations/webhooks/components/webhook-settings-tab.tsx index bb4000a0f0..9645463fa3 100644 --- a/apps/web/modules/integrations/webhooks/components/webhook-settings-tab.tsx +++ b/apps/web/modules/integrations/webhooks/components/webhook-settings-tab.tsx @@ -21,14 +21,14 @@ import { TSurvey } from "@formbricks/types/surveys/types"; import { deleteWebhookAction, testEndpointAction, updateWebhookAction } from "../actions"; import { TWebhookInput } from "../types/webhooks"; -interface ActionSettingsTabProps { +interface WebhookSettingsTabProps { webhook: Webhook; surveys: TSurvey[]; setOpen: (v: boolean) => void; isReadOnly: boolean; } -export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: ActionSettingsTabProps) => { +export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: WebhookSettingsTabProps) => { const { t } = useTranslate(); const router = useRouter(); const { register, handleSubmit } = useForm({ @@ -219,7 +219,7 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: Ac

- {webhook.source === "user" && !isReadOnly && ( + {!isReadOnly && ( - @@ -233,7 +238,11 @@ export const EmailCustomizationSettings = ({
-
Logo key; + +vi.mock("@formbricks/lib/constants", () => ({ + IS_FORMBRICKS_CLOUD: false, + FB_LOGO_URL: "https://example.com/mock-logo.png", + IMPRINT_URL: "https://example.com/imprint", + PRIVACY_URL: "https://example.com/privacy", + IMPRINT_ADDRESS: "Imprint Address", +})); + +const defaultProps = { + children:
Test Content
, + logoUrl: "https://example.com/custom-logo.png", + t: mockTranslate, +}; + +describe("EmailTemplate", () => { + beforeEach(() => { + cleanup(); + }); + + it("renders the default logo if no custom logo is provided", async () => { + const emailTemplateElement = await EmailTemplate({ + children:
Test Content
, + logoUrl: undefined, + t: mockTranslate, + }); + + render(emailTemplateElement); + + const logoImage = screen.getByTestId("default-logo-image"); + expect(logoImage).toBeInTheDocument(); + expect(logoImage).toHaveAttribute("src", "https://example.com/mock-logo.png"); + }); + + it("renders the custom logo if provided", async () => { + const emailTemplateElement = await EmailTemplate({ + ...defaultProps, + }); + + render(emailTemplateElement); + + const logoImage = screen.getByTestId("logo-image"); + expect(logoImage).toBeInTheDocument(); + expect(logoImage).toHaveAttribute("src", "https://example.com/custom-logo.png"); + }); + + it("renders the children content", async () => { + const emailTemplateElement = await EmailTemplate({ + ...defaultProps, + }); + + render(emailTemplateElement); + + expect(screen.getByTestId("child-text")).toBeInTheDocument(); + }); + + it("renders the imprint and privacy policy links if provided", async () => { + const emailTemplateElement = await EmailTemplate({ + ...defaultProps, + }); + + render(emailTemplateElement); + + expect(screen.getByText("emails.imprint")).toBeInTheDocument(); + expect(screen.getByText("emails.privacy_policy")).toBeInTheDocument(); + }); + + it("renders the imprint address if provided", async () => { + const emailTemplateElement = await EmailTemplate({ + ...defaultProps, + }); + + render(emailTemplateElement); + + expect(screen.getByText("emails.email_template_text_1")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/email/components/email-template.tsx b/apps/web/modules/email/components/email-template.tsx index 87811b7669..92ad28ab29 100644 --- a/apps/web/modules/email/components/email-template.tsx +++ b/apps/web/modules/email/components/email-template.tsx @@ -1,15 +1,15 @@ import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components"; import { TFnType } from "@tolgee/react"; -import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; +import React from "react"; +import { FB_LOGO_URL, IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; -const fbLogoUrl = - "https://s3.eu-central-1.amazonaws.com/listmonk-formbricks/Formbricks-Light-transparent.png"; +const fbLogoUrl = FB_LOGO_URL; const logoLink = "https://formbricks.com?utm_source=email_header&utm_medium=email"; interface EmailTemplateProps { - children: React.ReactNode; - logoUrl?: string; - t: TFnType; + readonly children: React.ReactNode; + readonly logoUrl?: string; + readonly t: TFnType; } export async function EmailTemplate({ @@ -30,10 +30,15 @@ export async function EmailTemplate({
{isDefaultLogo ? ( - Logo + Logo ) : ( - Logo + Logo )}
diff --git a/apps/web/modules/email/emails/survey/follow-up.test.tsx b/apps/web/modules/email/emails/survey/follow-up.test.tsx new file mode 100644 index 0000000000..f6b9c62321 --- /dev/null +++ b/apps/web/modules/email/emails/survey/follow-up.test.tsx @@ -0,0 +1,88 @@ +import { getTranslate } from "@/tolgee/server"; +import "@testing-library/jest-dom/vitest"; +import { render, screen } from "@testing-library/react"; +import { DefaultParamType, TFnType, TranslationKey } from "@tolgee/react/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { FollowUpEmail } from "./follow-up"; + +vi.mock("@formbricks/lib/constants", () => ({ + IS_FORMBRICKS_CLOUD: false, + FB_LOGO_URL: "https://example.com/mock-logo.png", + IMPRINT_URL: "https://example.com/imprint", + PRIVACY_URL: "https://example.com/privacy", + IMPRINT_ADDRESS: "Imprint Address", +})); + +vi.mock("@/tolgee/server", () => ({ + getTranslate: vi.fn(), +})); + +const defaultProps = { + html: "

Test HTML Content

", + logoUrl: "https://example.com/custom-logo.png", +}; + +describe("FollowUpEmail", () => { + beforeEach(() => { + vi.mocked(getTranslate).mockResolvedValue( + ((key: string) => key) as TFnType + ); + }); + + it("renders the default logo if no custom logo is provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + logoUrl: undefined, + }); + + render(followUpEmailElement); + + const logoImage = screen.getByAltText("Logo"); + expect(logoImage).toBeInTheDocument(); + expect(logoImage).toHaveAttribute("src", "https://example.com/mock-logo.png"); + }); + + it("renders the custom logo if provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + const logoImage = screen.getByAltText("Logo"); + expect(logoImage).toBeInTheDocument(); + expect(logoImage).toHaveAttribute("src", "https://example.com/custom-logo.png"); + }); + + it("renders the HTML content", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + expect(screen.getByText("Test HTML Content")).toBeInTheDocument(); + }); + + it("renders the imprint and privacy policy links if provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + expect(screen.getByText("emails.imprint")).toBeInTheDocument(); + expect(screen.getByText("emails.privacy_policy")).toBeInTheDocument(); + }); + + it("renders the imprint address if provided", async () => { + const followUpEmailElement = await FollowUpEmail({ + ...defaultProps, + }); + + render(followUpEmailElement); + + expect(screen.getByText("emails.powered_by_formbricks")).toBeInTheDocument(); + expect(screen.getByText("Imprint Address")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/email/emails/survey/follow-up.tsx b/apps/web/modules/email/emails/survey/follow-up.tsx index 613a2575cf..fed887ea88 100644 --- a/apps/web/modules/email/emails/survey/follow-up.tsx +++ b/apps/web/modules/email/emails/survey/follow-up.tsx @@ -1,15 +1,22 @@ import { getTranslate } from "@/tolgee/server"; import { Body, Container, Html, Img, Link, Section, Tailwind, Text } from "@react-email/components"; import dompurify from "isomorphic-dompurify"; -import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; +import React from "react"; +import { FB_LOGO_URL, IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; + +const fbLogoUrl = FB_LOGO_URL; +const logoLink = "https://formbricks.com?utm_source=email_header&utm_medium=email"; interface FollowUpEmailProps { - html: string; - logoUrl?: string; + readonly html: string; + readonly logoUrl?: string; } export async function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): Promise { const t = await getTranslate(); + console.log(t("emails.imprint")); + const isDefaultLogo = !logoUrl || logoUrl === fbLogoUrl; + return ( @@ -18,11 +25,15 @@ export async function FollowUpEmail({ html, logoUrl }: FollowUpEmailProps): Prom style={{ fontFamily: "'Jost', 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'sans-serif'", }}> - {logoUrl && ( -
+
+ {isDefaultLogo ? ( + + Logo + + ) : ( Logo -
- )} + )} +
Click or drag to upload files.

({ // mock react cache const testCache = (func: T) => func; -vi.mock("react", () => { - const originalModule = vi.importActual("react"); +vi.mock("react", async () => { + const react = await vi.importActual("react"); + return { - ...originalModule, + ...react, cache: testCache, }; }); +vi.mock("@tolgee/react", () => ({ + useTranslate: () => { + return { + t: (key: string) => key, + }; + }, +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + prefetch: vi.fn(), + refresh: vi.fn(), + }), +})); + // mock server-only vi.mock("server-only", () => { return {}; }); +vi.mock("@prisma/client", async () => { + const actual = await vi.importActual("@prisma/client"); + + return { + ...actual, + Prisma: actual.Prisma, + PrismaClient: class { + $connect() { + return Promise.resolve(); + } + $disconnect() { + return Promise.resolve(); + } + $extends() { + return this; + } + }, + }; +}); + +if (typeof URL.revokeObjectURL !== "function") { + URL.revokeObjectURL = () => {}; +} + +if (typeof URL.createObjectURL !== "function") { + URL.createObjectURL = () => "blob://fake-url"; +} + beforeEach(() => { vi.resetModules(); vi.resetAllMocks(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a8c63488b..5dd8164b33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,6 +330,9 @@ importers: '@tanstack/react-table': specifier: 8.20.6 version: 8.20.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@testing-library/jest-dom': + specifier: 6.6.3 + version: 6.6.3 '@tolgee/cli': specifier: 2.8.1 version: 2.8.1(jiti@2.4.1)(typescript@5.7.2) @@ -532,6 +535,9 @@ importers: '@neshca/cache-handler': specifier: 1.9.0 version: 1.9.0(next@15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0) + '@testing-library/react': + specifier: 16.2.0 + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/bcryptjs': specifier: 2.4.6 version: 2.4.6 @@ -553,6 +559,9 @@ importers: '@types/qrcode': specifier: 1.5.5 version: 1.5.5 + '@types/testing-library__react': + specifier: 10.2.0 + version: 10.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@vitest/coverage-v8': specifier: 2.1.8 version: 2.1.8(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) @@ -5575,6 +5584,25 @@ packages: resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.2.0': + resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@testing-library/user-event@14.5.2': resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} engines: {node: '>=12', npm: '>=6'} @@ -5816,6 +5844,10 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/testing-library__react@10.2.0': + resolution: {integrity: sha512-KbU7qVfEwml8G5KFxM+xEfentAAVj/SOQSjW0+HqzjPE0cXpt0IpSamfX4jGYCImznDHgQcfXBPajS7HjLZduw==} + deprecated: This is a stub types definition. testing-library__react provides its own type definitions, so you do not need this installed. + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -20003,6 +20035,26 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.1 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.7 + '@testing-library/dom': 10.4.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: '@testing-library/dom': 10.4.0 @@ -20263,6 +20315,16 @@ snapshots: dependencies: '@types/node': 22.10.2 + '@types/testing-library__react@10.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@testing-library/react': 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - '@testing-library/dom' + - '@types/react' + - '@types/react-dom' + - react + - react-dom + '@types/trusted-types@2.0.7': optional: true diff --git a/sonar-project.properties b/sonar-project.properties index 897155f65b..e7a41cb5a7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -9,8 +9,6 @@ sonar.exclusions=**/node_modules/**,**/.next/**,**/dist/**,**/build/**,**/*.test sonar.tests=apps/web sonar.test.inclusions=**/*.test.*,**/*.spec.* sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info -sonar.coverage.exclusions=**/constants.ts,**/route.ts,**/openapi.ts,**/openapi-document.ts,modules/**/types/**,playwright/**,**/*.test.*,**/*.spec.* -sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,**/openapi.ts,**/openapi-document.ts,modules/**/types/** # TypeScript configuration sonar.typescript.tsconfigPath=apps/web/tsconfig.json @@ -23,5 +21,5 @@ sonar.scm.exclusions.disabled=false sonar.sourceEncoding=UTF-8 # Coverage -sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/** -sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/** \ No newline at end of file +sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/** +sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/constants.ts,**/route.ts,modules/**/types/**,**/openapi.ts,**/openapi-document.ts From 48a92f3e554c50519ba0a217296f8e00b43d63c8 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:12:00 +0530 Subject: [PATCH 022/411] feat: OIDC name fields added (#4872) Co-authored-by: pandeymangg --- apps/web/modules/auth/types/auth.ts | 5 + apps/web/modules/ee/sso/lib/sso-handlers.ts | 16 +- .../lib/tests/__mock__/sso-handlers.mock.ts | 89 +++++ .../ee/sso/lib/tests/sso-handlers.test.ts | 357 ++++++++++++++++++ apps/web/vite.config.mts | 51 +-- 5 files changed, 492 insertions(+), 26 deletions(-) create mode 100644 apps/web/modules/auth/types/auth.ts create mode 100644 apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts create mode 100644 apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts diff --git a/apps/web/modules/auth/types/auth.ts b/apps/web/modules/auth/types/auth.ts new file mode 100644 index 0000000000..e8aad424af --- /dev/null +++ b/apps/web/modules/auth/types/auth.ts @@ -0,0 +1,5 @@ +export type TOidcNameFields = { + given_name?: string; + family_name?: string; + preferred_username?: string; +}; diff --git a/apps/web/modules/ee/sso/lib/sso-handlers.ts b/apps/web/modules/ee/sso/lib/sso-handlers.ts index fffcd3d9d4..af40ab23dc 100644 --- a/apps/web/modules/ee/sso/lib/sso-handlers.ts +++ b/apps/web/modules/ee/sso/lib/sso-handlers.ts @@ -1,6 +1,7 @@ import { createBrevoCustomer } from "@/modules/auth/lib/brevo"; import { getUserByEmail, updateUser } from "@/modules/auth/lib/user"; import { createUser } from "@/modules/auth/lib/user"; +import { TOidcNameFields } from "@/modules/auth/types/auth"; import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils"; import type { IdentityProvider } from "@prisma/client"; import type { Account } from "next-auth"; @@ -79,9 +80,22 @@ export const handleSSOCallback = async ({ user, account }: { user: TUser; accoun return true; } + let userName = user.name; + + if (provider === "openid") { + const oidcUser = user as TUser & TOidcNameFields; + if (oidcUser.name) { + userName = oidcUser.name; + } else if (oidcUser.given_name || oidcUser.family_name) { + userName = `${oidcUser.given_name} ${oidcUser.family_name}`; + } else if (oidcUser.preferred_username) { + userName = oidcUser.preferred_username; + } + } + const userProfile = await createUser({ name: - user.name || + userName || user.email .split("@")[0] .replace(/[^'\p{L}\p{M}\s\d-]+/gu, " ") diff --git a/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts new file mode 100644 index 0000000000..a35d700c14 --- /dev/null +++ b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts @@ -0,0 +1,89 @@ +import { Organization } from "@prisma/client"; +import type { Account } from "next-auth"; +import type { TUser } from "@formbricks/types/user"; + +// Mock user data +export const mockUser: TUser = { + id: "user-123", + email: "test@example.com", + name: "Test User", + notificationSettings: { + alert: {}, + weeklySummary: {}, + unsubscribedOrganizationIds: [], + }, + emailVerified: new Date(), + imageUrl: "https://example.com/image.png", + twoFactorEnabled: false, + identityProvider: "google", + locale: "en-US", + role: "other", + createdAt: new Date(), + updatedAt: new Date(), + objective: "improve_user_retention", +}; + +// Mock account data +export const mockAccount: Account = { + provider: "google", + type: "oauth", + providerAccountId: "provider-123", +}; + +// Mock OpenID account +export const mockOpenIdAccount: Account = { + ...mockAccount, + provider: "openid", +}; + +// Mock SAML account +export const mockSamlAccount: Account = { + ...mockAccount, + provider: "saml", +}; + +// Mock organization data +export const mockOrganization: Organization = { + id: "org-123", + name: "Test Organization", + isAIEnabled: false, + whitelabel: { + enabled: false, + }, + billing: { + stripeCustomerId: null, + plan: "free", + period: "monthly", + limits: { monthly: { responses: null, miu: null }, projects: null }, + periodStart: new Date(), + }, + createdAt: new Date(), + updatedAt: new Date(), +}; + +// Mock user with OpenID fields +export const mockOpenIdUser = (options?: { + name?: string; + given_name?: string; + family_name?: string; + preferred_username?: string; + email?: string; +}): TUser & { + given_name?: string; + family_name?: string; + preferred_username?: string; +} => ({ + ...mockUser, + name: options?.name || "", + given_name: options?.given_name, + family_name: options?.family_name, + preferred_username: options?.preferred_username, + email: options?.email || mockUser.email, +}); + +// Mock created user response +export const mockCreatedUser = (name: string = mockUser.name): TUser => ({ + ...mockUser, + name, + emailVerified: new Date(), +}); diff --git a/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts b/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts new file mode 100644 index 0000000000..0941a52ac3 --- /dev/null +++ b/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts @@ -0,0 +1,357 @@ +import { createBrevoCustomer } from "@/modules/auth/lib/brevo"; +import { createUser, getUserByEmail, updateUser } from "@/modules/auth/lib/user"; +import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { createAccount } from "@formbricks/lib/account/service"; +import { createMembership } from "@formbricks/lib/membership/service"; +import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; +import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import { handleSSOCallback } from "../sso-handlers"; +import { + mockAccount, + mockCreatedUser, + mockOpenIdAccount, + mockOpenIdUser, + mockOrganization, + mockSamlAccount, + mockUser, +} from "./__mock__/sso-handlers.mock"; + +// Mock all dependencies +vi.mock("@/modules/auth/lib/brevo", () => ({ + createBrevoCustomer: vi.fn(), +})); + +vi.mock("@/modules/auth/lib/user", () => ({ + getUserByEmail: vi.fn(), + updateUser: vi.fn(), + createUser: vi.fn(), +})); + +vi.mock("@/modules/ee/license-check/lib/utils", () => ({ + getIsSamlSsoEnabled: vi.fn(), + getisSsoEnabled: vi.fn(), +})); + +vi.mock("@formbricks/database", () => ({ + prisma: { + user: { + findFirst: vi.fn(), + }, + }, +})); + +vi.mock("@formbricks/lib/account/service", () => ({ + createAccount: vi.fn(), +})); + +vi.mock("@formbricks/lib/membership/service", () => ({ + createMembership: vi.fn(), +})); + +vi.mock("@formbricks/lib/organization/service", () => ({ + createOrganization: vi.fn(), + getOrganization: vi.fn(), +})); + +vi.mock("@formbricks/lib/utils/locale", () => ({ + findMatchingLocale: vi.fn(), +})); + +// Mock environment variables +vi.mock("@formbricks/lib/constants", () => ({ + DEFAULT_ORGANIZATION_ID: "org-123", + DEFAULT_ORGANIZATION_ROLE: "member", +})); + +describe("handleSSOCallback", () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Default mock implementations + vi.mocked(getisSsoEnabled).mockResolvedValue(true); + vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true); + vi.mocked(findMatchingLocale).mockResolvedValue("en-US"); + + // Mock organization-related functions + vi.mocked(getOrganization).mockResolvedValue(mockOrganization); + vi.mocked(createOrganization).mockResolvedValue(mockOrganization); + vi.mocked(createMembership).mockResolvedValue({ + role: "member", + accepted: true, + userId: mockUser.id, + organizationId: mockOrganization.id, + }); + vi.mocked(updateUser).mockResolvedValue({ ...mockUser, id: "user-123" }); + }); + + describe("Early return conditions", () => { + it("should return false if SSO is not enabled", async () => { + vi.mocked(getisSsoEnabled).mockResolvedValue(false); + + const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + + expect(result).toBe(false); + expect(getisSsoEnabled).toHaveBeenCalled(); + }); + + it("should return false if user email is missing", async () => { + const userWithoutEmail = { ...mockUser, email: "" }; + + const result = await handleSSOCallback({ user: userWithoutEmail, account: mockAccount }); + + expect(result).toBe(false); + }); + + it("should return false if account type is not oauth", async () => { + const nonOauthAccount = { ...mockAccount, type: "credentials" as const }; + + const result = await handleSSOCallback({ user: mockUser, account: nonOauthAccount }); + + expect(result).toBe(false); + }); + + it("should return false if provider is SAML and SAML SSO is not enabled", async () => { + vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(false); + + const result = await handleSSOCallback({ user: mockUser, account: mockSamlAccount }); + + expect(result).toBe(false); + expect(getIsSamlSsoEnabled).toHaveBeenCalled(); + }); + }); + + describe("Existing user handling", () => { + it("should return true if user with account already exists and email is the same", async () => { + vi.mocked(prisma.user.findFirst).mockResolvedValue({ + ...mockUser, + email: mockUser.email, + accounts: [{ provider: mockAccount.provider }], + }); + + const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + + expect(result).toBe(true); + expect(prisma.user.findFirst).toHaveBeenCalledWith({ + include: { + accounts: { + where: { + provider: mockAccount.provider, + }, + }, + }, + where: { + identityProvider: mockAccount.provider.toLowerCase().replace("-", ""), + identityProviderAccountId: mockAccount.providerAccountId, + }, + }); + }); + + it("should update user email if user with account exists but email changed", async () => { + const existingUser = { + ...mockUser, + id: "existing-user-id", + email: "old-email@example.com", + accounts: [{ provider: mockAccount.provider }], + }; + + vi.mocked(prisma.user.findFirst).mockResolvedValue(existingUser); + vi.mocked(getUserByEmail).mockResolvedValue(null); + vi.mocked(updateUser).mockResolvedValue({ ...existingUser, email: mockUser.email }); + + const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + + expect(result).toBe(true); + expect(updateUser).toHaveBeenCalledWith(existingUser.id, { email: mockUser.email }); + }); + + it("should throw error if user with account exists, email changed, and another user has the new email", async () => { + const existingUser = { + ...mockUser, + id: "existing-user-id", + email: "old-email@example.com", + accounts: [{ provider: mockAccount.provider }], + }; + + vi.mocked(prisma.user.findFirst).mockResolvedValue(existingUser); + vi.mocked(getUserByEmail).mockResolvedValue({ + id: "another-user-id", + email: mockUser.email, + emailVerified: mockUser.emailVerified, + locale: mockUser.locale, + }); + + await expect(handleSSOCallback({ user: mockUser, account: mockAccount })).rejects.toThrow( + "Looks like you updated your email somewhere else. A user with this new email exists already." + ); + }); + + it("should return true if user with email already exists", async () => { + vi.mocked(prisma.user.findFirst).mockResolvedValue(null); + vi.mocked(getUserByEmail).mockResolvedValue({ + id: "existing-user-id", + email: mockUser.email, + emailVerified: mockUser.emailVerified, + locale: mockUser.locale, + }); + + const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + + expect(result).toBe(true); + }); + }); + + describe("New user creation", () => { + it("should create a new user if no existing user found", async () => { + vi.mocked(prisma.user.findFirst).mockResolvedValue(null); + vi.mocked(getUserByEmail).mockResolvedValue(null); + vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); + + const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + + expect(result).toBe(true); + expect(createUser).toHaveBeenCalledWith({ + name: mockUser.name, + email: mockUser.email, + emailVerified: expect.any(Date), + identityProvider: mockAccount.provider.toLowerCase().replace("-", ""), + identityProviderAccountId: mockAccount.providerAccountId, + locale: "en-US", + }); + expect(createBrevoCustomer).toHaveBeenCalledWith({ id: mockUser.id, email: mockUser.email }); + }); + + it("should create organization and membership for new user when DEFAULT_ORGANIZATION_ID is set", async () => { + vi.mocked(prisma.user.findFirst).mockResolvedValue(null); + vi.mocked(getUserByEmail).mockResolvedValue(null); + vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); + vi.mocked(getOrganization).mockResolvedValue(null); + + const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + + expect(result).toBe(true); + expect(createOrganization).toHaveBeenCalledWith({ + id: "org-123", + name: expect.stringContaining("Organization"), + }); + expect(createMembership).toHaveBeenCalledWith("org-123", mockCreatedUser().id, { + role: "owner", + accepted: true, + }); + expect(createAccount).toHaveBeenCalledWith({ + ...mockAccount, + userId: mockCreatedUser().id, + }); + expect(updateUser).toHaveBeenCalledWith(mockCreatedUser().id, { + notificationSettings: expect.objectContaining({ + unsubscribedOrganizationIds: ["org-123"], + }), + }); + }); + + it("should use existing organization if it exists", async () => { + vi.mocked(prisma.user.findFirst).mockResolvedValue(null); + vi.mocked(getUserByEmail).mockResolvedValue(null); + vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); + + const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + + expect(result).toBe(true); + expect(createOrganization).not.toHaveBeenCalled(); + expect(createMembership).toHaveBeenCalledWith(mockOrganization.id, mockCreatedUser().id, { + role: "member", + accepted: true, + }); + }); + }); + + describe("OpenID Connect name handling", () => { + it("should use oidcUser.name when available", async () => { + const openIdUser = mockOpenIdUser({ + name: "Direct Name", + given_name: "John", + family_name: "Doe", + }); + + vi.mocked(createUser).mockResolvedValue(mockCreatedUser("Direct Name")); + + const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + + expect(result).toBe(true); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ + name: "Direct Name", + email: openIdUser.email, + identityProvider: "openid", + }) + ); + }); + + it("should use given_name + family_name when name is not available", async () => { + const openIdUser = mockOpenIdUser({ + name: undefined, + given_name: "John", + family_name: "Doe", + }); + + vi.mocked(createUser).mockResolvedValue(mockCreatedUser("John Doe")); + + const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + + expect(result).toBe(true); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ + name: "John Doe", + email: openIdUser.email, + identityProvider: "openid", + }) + ); + }); + + it("should use preferred_username when name and given_name/family_name are not available", async () => { + const openIdUser = mockOpenIdUser({ + name: undefined, + given_name: undefined, + family_name: undefined, + preferred_username: "preferred.user", + }); + + vi.mocked(createUser).mockResolvedValue(mockCreatedUser("preferred.user")); + + const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + + expect(result).toBe(true); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ + name: "preferred.user", + email: openIdUser.email, + identityProvider: "openid", + }) + ); + }); + + it("should fallback to email username when no OIDC name fields are available", async () => { + const openIdUser = mockOpenIdUser({ + name: undefined, + given_name: undefined, + family_name: undefined, + preferred_username: undefined, + email: "test.user@example.com", + }); + + vi.mocked(createUser).mockResolvedValue(mockCreatedUser("test.user")); + + const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + + expect(result).toBe(true); + + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ + email: openIdUser.email, + identityProvider: "openid", + }) + ); + }); + }); +}); diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts index 158161baa0..7bedcd3e54 100644 --- a/apps/web/vite.config.mts +++ b/apps/web/vite.config.mts @@ -1,7 +1,7 @@ // vitest.config.ts -import tsconfigPaths from 'vite-tsconfig-paths'; -import { loadEnv } from 'vite' -import { defineConfig } from 'vitest/config'; +import { loadEnv } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { @@ -10,33 +10,34 @@ export default defineConfig({ ["**/page.test.tsx", "node"], // page files use node environment because it uses server-side rendering ["**/*.test.tsx", "jsdom"], ], - exclude: ['playwright/**', 'node_modules/**'], - setupFiles: ['../../packages/lib/vitestSetup.ts'], - env: loadEnv('', process.cwd(), ''), + exclude: ["playwright/**", "node_modules/**"], + setupFiles: ["../../packages/lib/vitestSetup.ts"], + env: loadEnv("", process.cwd(), ""), coverage: { - provider: 'v8', // Use V8 as the coverage provider - reporter: ['text', 'html', 'lcov'], // Generate text summary and HTML reports - reportsDirectory: './coverage', // Output coverage reports to the coverage/ directory + provider: "v8", // Use V8 as the coverage provider + reporter: ["text", "html", "lcov"], // Generate text summary and HTML reports + reportsDirectory: "./coverage", // Output coverage reports to the coverage/ directory include: [ - 'modules/api/v2/**/*.ts', - 'modules/auth/lib/**/*.ts', - 'modules/signup/lib/**/*.ts', - 'modules/ee/whitelabel/email-customization/components/*.tsx', - 'modules/email/components/email-template.tsx', - 'modules/email/emails/survey/follow-up.tsx', - 'app/(app)/environments/**/settings/(organization)/general/page.tsx', + "modules/api/v2/**/*.ts", + "modules/auth/lib/**/*.ts", + "modules/signup/lib/**/*.ts", + "modules/ee/whitelabel/email-customization/components/*.tsx", + "modules/email/components/email-template.tsx", + "modules/email/emails/survey/follow-up.tsx", + "app/(app)/environments/**/settings/(organization)/general/page.tsx", + "modules/ee/sso/lib/**/*.ts", ], exclude: [ - '**/.next/**', - '**/*.test.*', - '**/*.spec.*', - '**/constants.ts', // Exclude constants files - '**/route.ts', // Exclude route files - '**/openapi.ts', // Exclude openapi configuration files - '**/openapi-document.ts', // Exclude openapi document files - 'modules/**/types/**', // Exclude types + "**/.next/**", + "**/*.test.*", + "**/*.spec.*", + "**/constants.ts", // Exclude constants files + "**/route.ts", // Exclude route files + "**/openapi.ts", // Exclude openapi configuration files + "**/openapi-document.ts", // Exclude openapi document files + "modules/**/types/**", // Exclude types ], }, }, plugins: [tsconfigPaths()], -}); \ No newline at end of file +}); From 333372d61c7f3efc7324e66b97b934aa13ee01fb Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:15:28 +0530 Subject: [PATCH 023/411] fix: removed link survey identification from share modal (#4898) Co-authored-by: Dhruwang --- .../responses/components/ResponseTableColumns.tsx | 7 ------- .../summary/components/shareEmbedModal/LinkTab.tsx | 5 ----- .../ShareSurveyLink/components/SurveyLinkDisplay.tsx | 2 +- .../modules/analysis/components/ShareSurveyLink/index.tsx | 2 +- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx index 634c3206b2..c1eb5af132 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns.tsx @@ -200,13 +200,6 @@ export const generateResponseTableColumns = ( {t("environments.surveys.responses.how_to_identify_users")} - - {t("common.link_surveys")} - {" "} - or{" "} { const { t } = useTranslate(); const docsLinks = [ - { - title: t("environments.surveys.summary.identify_users"), - description: t("environments.surveys.summary.identify_users_description"), - link: "https://formbricks.com/docs/link-surveys/user-identification", - }, { title: t("environments.surveys.summary.data_prefilling"), description: t("environments.surveys.summary.data_prefilling_description"), diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx index 629db33b2f..93b3b7062f 100644 --- a/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx +++ b/apps/web/modules/analysis/components/ShareSurveyLink/components/SurveyLinkDisplay.tsx @@ -8,7 +8,7 @@ export const SurveyLinkDisplay = ({ surveyUrl }: SurveyLinkDisplayProps) => { return ( ); diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx index 69bb2f09d8..33528d9f51 100644 --- a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx +++ b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx @@ -71,7 +71,7 @@ export const ShareSurveyLink = ({ return (
- +
{isBrandingEnabled ? : null} - {showProgressBar ? : null} + {showProgressBar ? :
}
diff --git a/packages/surveys/src/components/questions/nps-question.tsx b/packages/surveys/src/components/questions/nps-question.tsx index 95fb806e1a..4df3fa32c0 100644 --- a/packages/surveys/src/components/questions/nps-question.tsx +++ b/packages/surveys/src/components/questions/nps-question.tsx @@ -156,7 +156,7 @@ export function NPSQuestion({
{question.required ? ( - <> +
) : (
{question.required ? ( - <> +
) : ( + className={cn("fb-overflow-auto fb-px-4 fb-pb-6 fb-bg-survey-bg")}> {children}
{!isAtBottom && ( From 828e23b5c697bf457f54c72b26f84b325542ff9c Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Wed, 12 Mar 2025 17:22:15 +0530 Subject: [PATCH 030/411] fix: survey autoclose on inactivity fix (#4916) Co-authored-by: Dhruwang --- .../surveys/src/components/general/survey.tsx | 9 +++++- .../wrappers/auto-close-wrapper.tsx | 28 +++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/surveys/src/components/general/survey.tsx b/packages/surveys/src/components/general/survey.tsx index 011953f053..7ddc39326b 100644 --- a/packages/surveys/src/components/general/survey.tsx +++ b/packages/surveys/src/components/general/survey.tsx @@ -120,6 +120,8 @@ export function Survey({ return null; }, [apiHost, environmentId, getSetIsError, getSetIsResponseSendingFinished, surveyState]); + const [hasInteracted, setHasInteracted] = useState(false); + const [localSurvey, setlocalSurvey] = useState(survey); const [currentVariables, setCurrentVariables] = useState({}); @@ -588,7 +590,12 @@ export function Survey({ }; return ( - +
void; - offset: number; children: React.ReactNode; + hasInteracted: boolean; + setHasInteracted: (hasInteracted: boolean) => void; } -export function AutoCloseWrapper({ survey, onClose, children, offset }: AutoCloseProps) { +export function AutoCloseWrapper({ + survey, + onClose, + children, + questionIdx, + hasInteracted, + setHasInteracted, +}: AutoCloseProps) { const [countDownActive, setCountDownActive] = useState(true); + const timeoutRef = useRef | null>(null); const isAppSurvey = survey.type === "app"; - const showAutoCloseProgressBar = countDownActive && isAppSurvey && offset === 0; + + const isFirstQuestion = useMemo(() => { + if (survey.welcomeCard.enabled) return questionIdx === -1; + return questionIdx === 0; + }, [questionIdx, survey.welcomeCard.enabled]); + + const showAutoCloseProgressBar = countDownActive && isAppSurvey && isFirstQuestion && !hasInteracted; const startCountdown = () => { - if (!survey.autoClose) return; + if (!survey.autoClose || !isFirstQuestion || hasInteracted) return; if (timeoutRef.current) { stopCountdown(); @@ -31,6 +47,8 @@ export function AutoCloseWrapper({ survey, onClose, children, offset }: AutoClos const stopCountdown = () => { setCountDownActive(false); + setHasInteracted(true); // Mark that user has interacted + if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; From a25e5dcfcd8836c0048472cf032f43708c6c499e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:51:57 +0100 Subject: [PATCH 031/411] chore(deps-dev): bump esbuild from 0.25.0 to 0.25.1 in the npm_and_yarn group across 1 directory (#4911) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/storybook/package.json | 2 +- pnpm-lock.yaml | 223 +++++++++++++++++++----------------- 2 files changed, 116 insertions(+), 109 deletions(-) diff --git a/apps/storybook/package.json b/apps/storybook/package.json index aa7ffafa17..5ffdc138d3 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -30,7 +30,7 @@ "@typescript-eslint/eslint-plugin": "8.18.0", "@typescript-eslint/parser": "8.18.0", "@vitejs/plugin-react": "4.3.4", - "esbuild": "0.25.0", + "esbuild": "0.25.1", "eslint-plugin-storybook": "0.11.1", "prop-types": "15.8.1", "storybook": "8.4.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dd8164b33..4e70f7d77d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,8 +160,8 @@ importers: specifier: 4.3.4 version: 4.3.4(vite@6.0.9(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) esbuild: - specifier: 0.25.0 - version: 0.25.0 + specifier: 0.25.1 + version: 0.25.1 eslint-plugin-storybook: specifier: 0.11.1 version: 0.11.1(eslint@8.57.0)(typescript@5.7.2) @@ -2333,8 +2333,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.0': - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + '@esbuild/aix-ppc64@0.25.1': + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -2357,8 +2357,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.0': - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + '@esbuild/android-arm64@0.25.1': + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -2381,8 +2381,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.0': - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + '@esbuild/android-arm@0.25.1': + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -2405,8 +2405,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.0': - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + '@esbuild/android-x64@0.25.1': + resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -2429,8 +2429,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.0': - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + '@esbuild/darwin-arm64@0.25.1': + resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -2453,8 +2453,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.0': - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + '@esbuild/darwin-x64@0.25.1': + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -2477,8 +2477,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.0': - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + '@esbuild/freebsd-arm64@0.25.1': + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -2501,8 +2501,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.0': - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + '@esbuild/freebsd-x64@0.25.1': + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -2525,8 +2525,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.0': - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + '@esbuild/linux-arm64@0.25.1': + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -2549,8 +2549,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.0': - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + '@esbuild/linux-arm@0.25.1': + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -2573,8 +2573,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.0': - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + '@esbuild/linux-ia32@0.25.1': + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -2597,8 +2597,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.0': - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + '@esbuild/linux-loong64@0.25.1': + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -2621,8 +2621,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.0': - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + '@esbuild/linux-mips64el@0.25.1': + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -2645,8 +2645,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.0': - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + '@esbuild/linux-ppc64@0.25.1': + resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -2669,8 +2669,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.0': - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + '@esbuild/linux-riscv64@0.25.1': + resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -2693,8 +2693,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.0': - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + '@esbuild/linux-s390x@0.25.1': + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -2717,8 +2717,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.0': - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + '@esbuild/linux-x64@0.25.1': + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -2729,8 +2729,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.25.0': - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + '@esbuild/netbsd-arm64@0.25.1': + resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -2753,8 +2753,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.0': - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + '@esbuild/netbsd-x64@0.25.1': + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -2771,8 +2771,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.25.0': - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + '@esbuild/openbsd-arm64@0.25.1': + resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -2795,8 +2795,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + '@esbuild/openbsd-x64@0.25.1': + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -2819,8 +2819,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.0': - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + '@esbuild/sunos-x64@0.25.1': + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -2843,8 +2843,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.0': - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + '@esbuild/win32-arm64@0.25.1': + resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -2867,8 +2867,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.0': - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + '@esbuild/win32-ia32@0.25.1': + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -2891,8 +2891,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + '@esbuild/win32-x64@0.25.1': + resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -7374,6 +7374,9 @@ packages: core-js-compat@3.40.0: resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} + core-js-compat@3.41.0: + resolution: {integrity: sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==} + core-js@3.40.0: resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} @@ -7904,8 +7907,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + esbuild@0.25.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} engines: {node: '>=18'} hasBin: true @@ -15202,7 +15205,7 @@ snapshots: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.26.5 - '@babel/template': 7.26.9 + '@babel/template': 7.25.9 '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': dependencies: @@ -15602,7 +15605,7 @@ snapshots: babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) - core-js-compat: 3.40.0 + core-js-compat: 3.41.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -16019,7 +16022,7 @@ snapshots: '@esbuild/aix-ppc64@0.24.2': optional: true - '@esbuild/aix-ppc64@0.25.0': + '@esbuild/aix-ppc64@0.25.1': optional: true '@esbuild/android-arm64@0.21.5': @@ -16031,7 +16034,7 @@ snapshots: '@esbuild/android-arm64@0.24.2': optional: true - '@esbuild/android-arm64@0.25.0': + '@esbuild/android-arm64@0.25.1': optional: true '@esbuild/android-arm@0.21.5': @@ -16043,7 +16046,7 @@ snapshots: '@esbuild/android-arm@0.24.2': optional: true - '@esbuild/android-arm@0.25.0': + '@esbuild/android-arm@0.25.1': optional: true '@esbuild/android-x64@0.21.5': @@ -16055,7 +16058,7 @@ snapshots: '@esbuild/android-x64@0.24.2': optional: true - '@esbuild/android-x64@0.25.0': + '@esbuild/android-x64@0.25.1': optional: true '@esbuild/darwin-arm64@0.21.5': @@ -16067,7 +16070,7 @@ snapshots: '@esbuild/darwin-arm64@0.24.2': optional: true - '@esbuild/darwin-arm64@0.25.0': + '@esbuild/darwin-arm64@0.25.1': optional: true '@esbuild/darwin-x64@0.21.5': @@ -16079,7 +16082,7 @@ snapshots: '@esbuild/darwin-x64@0.24.2': optional: true - '@esbuild/darwin-x64@0.25.0': + '@esbuild/darwin-x64@0.25.1': optional: true '@esbuild/freebsd-arm64@0.21.5': @@ -16091,7 +16094,7 @@ snapshots: '@esbuild/freebsd-arm64@0.24.2': optional: true - '@esbuild/freebsd-arm64@0.25.0': + '@esbuild/freebsd-arm64@0.25.1': optional: true '@esbuild/freebsd-x64@0.21.5': @@ -16103,7 +16106,7 @@ snapshots: '@esbuild/freebsd-x64@0.24.2': optional: true - '@esbuild/freebsd-x64@0.25.0': + '@esbuild/freebsd-x64@0.25.1': optional: true '@esbuild/linux-arm64@0.21.5': @@ -16115,7 +16118,7 @@ snapshots: '@esbuild/linux-arm64@0.24.2': optional: true - '@esbuild/linux-arm64@0.25.0': + '@esbuild/linux-arm64@0.25.1': optional: true '@esbuild/linux-arm@0.21.5': @@ -16127,7 +16130,7 @@ snapshots: '@esbuild/linux-arm@0.24.2': optional: true - '@esbuild/linux-arm@0.25.0': + '@esbuild/linux-arm@0.25.1': optional: true '@esbuild/linux-ia32@0.21.5': @@ -16139,7 +16142,7 @@ snapshots: '@esbuild/linux-ia32@0.24.2': optional: true - '@esbuild/linux-ia32@0.25.0': + '@esbuild/linux-ia32@0.25.1': optional: true '@esbuild/linux-loong64@0.21.5': @@ -16151,7 +16154,7 @@ snapshots: '@esbuild/linux-loong64@0.24.2': optional: true - '@esbuild/linux-loong64@0.25.0': + '@esbuild/linux-loong64@0.25.1': optional: true '@esbuild/linux-mips64el@0.21.5': @@ -16163,7 +16166,7 @@ snapshots: '@esbuild/linux-mips64el@0.24.2': optional: true - '@esbuild/linux-mips64el@0.25.0': + '@esbuild/linux-mips64el@0.25.1': optional: true '@esbuild/linux-ppc64@0.21.5': @@ -16175,7 +16178,7 @@ snapshots: '@esbuild/linux-ppc64@0.24.2': optional: true - '@esbuild/linux-ppc64@0.25.0': + '@esbuild/linux-ppc64@0.25.1': optional: true '@esbuild/linux-riscv64@0.21.5': @@ -16187,7 +16190,7 @@ snapshots: '@esbuild/linux-riscv64@0.24.2': optional: true - '@esbuild/linux-riscv64@0.25.0': + '@esbuild/linux-riscv64@0.25.1': optional: true '@esbuild/linux-s390x@0.21.5': @@ -16199,7 +16202,7 @@ snapshots: '@esbuild/linux-s390x@0.24.2': optional: true - '@esbuild/linux-s390x@0.25.0': + '@esbuild/linux-s390x@0.25.1': optional: true '@esbuild/linux-x64@0.21.5': @@ -16211,13 +16214,13 @@ snapshots: '@esbuild/linux-x64@0.24.2': optional: true - '@esbuild/linux-x64@0.25.0': + '@esbuild/linux-x64@0.25.1': optional: true '@esbuild/netbsd-arm64@0.24.2': optional: true - '@esbuild/netbsd-arm64@0.25.0': + '@esbuild/netbsd-arm64@0.25.1': optional: true '@esbuild/netbsd-x64@0.21.5': @@ -16229,7 +16232,7 @@ snapshots: '@esbuild/netbsd-x64@0.24.2': optional: true - '@esbuild/netbsd-x64@0.25.0': + '@esbuild/netbsd-x64@0.25.1': optional: true '@esbuild/openbsd-arm64@0.23.1': @@ -16238,7 +16241,7 @@ snapshots: '@esbuild/openbsd-arm64@0.24.2': optional: true - '@esbuild/openbsd-arm64@0.25.0': + '@esbuild/openbsd-arm64@0.25.1': optional: true '@esbuild/openbsd-x64@0.21.5': @@ -16250,7 +16253,7 @@ snapshots: '@esbuild/openbsd-x64@0.24.2': optional: true - '@esbuild/openbsd-x64@0.25.0': + '@esbuild/openbsd-x64@0.25.1': optional: true '@esbuild/sunos-x64@0.21.5': @@ -16262,7 +16265,7 @@ snapshots: '@esbuild/sunos-x64@0.24.2': optional: true - '@esbuild/sunos-x64@0.25.0': + '@esbuild/sunos-x64@0.25.1': optional: true '@esbuild/win32-arm64@0.21.5': @@ -16274,7 +16277,7 @@ snapshots: '@esbuild/win32-arm64@0.24.2': optional: true - '@esbuild/win32-arm64@0.25.0': + '@esbuild/win32-arm64@0.25.1': optional: true '@esbuild/win32-ia32@0.21.5': @@ -16286,7 +16289,7 @@ snapshots: '@esbuild/win32-ia32@0.24.2': optional: true - '@esbuild/win32-ia32@0.25.0': + '@esbuild/win32-ia32@0.25.1': optional: true '@esbuild/win32-x64@0.21.5': @@ -16298,7 +16301,7 @@ snapshots: '@esbuild/win32-x64@0.24.2': optional: true - '@esbuild/win32-x64@0.25.0': + '@esbuild/win32-x64@0.25.1': optional: true '@eslint-community/eslint-utils@4.4.1(eslint@8.57.0)': @@ -22173,6 +22176,10 @@ snapshots: dependencies: browserslist: 4.24.4 + core-js-compat@3.41.0: + dependencies: + browserslist: 4.24.4 + core-js@3.40.0: {} core-util-is@1.0.3: {} @@ -22821,33 +22828,33 @@ snapshots: '@esbuild/win32-ia32': 0.24.2 '@esbuild/win32-x64': 0.24.2 - esbuild@0.25.0: + esbuild@0.25.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 + '@esbuild/aix-ppc64': 0.25.1 + '@esbuild/android-arm': 0.25.1 + '@esbuild/android-arm64': 0.25.1 + '@esbuild/android-x64': 0.25.1 + '@esbuild/darwin-arm64': 0.25.1 + '@esbuild/darwin-x64': 0.25.1 + '@esbuild/freebsd-arm64': 0.25.1 + '@esbuild/freebsd-x64': 0.25.1 + '@esbuild/linux-arm': 0.25.1 + '@esbuild/linux-arm64': 0.25.1 + '@esbuild/linux-ia32': 0.25.1 + '@esbuild/linux-loong64': 0.25.1 + '@esbuild/linux-mips64el': 0.25.1 + '@esbuild/linux-ppc64': 0.25.1 + '@esbuild/linux-riscv64': 0.25.1 + '@esbuild/linux-s390x': 0.25.1 + '@esbuild/linux-x64': 0.25.1 + '@esbuild/netbsd-arm64': 0.25.1 + '@esbuild/netbsd-x64': 0.25.1 + '@esbuild/openbsd-arm64': 0.25.1 + '@esbuild/openbsd-x64': 0.25.1 + '@esbuild/sunos-x64': 0.25.1 + '@esbuild/win32-arm64': 0.25.1 + '@esbuild/win32-ia32': 0.25.1 + '@esbuild/win32-x64': 0.25.1 escalade@3.2.0: {} @@ -29224,7 +29231,7 @@ snapshots: vite@6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0): dependencies: - esbuild: 0.25.0 + esbuild: 0.25.1 postcss: 8.5.3 rollup: 4.34.8 optionalDependencies: From 4870dc8d4573a4079ceb521d54d7d9e912ceded7 Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Wed, 12 Mar 2025 05:40:01 -0700 Subject: [PATCH 032/411] fix: update project config navbar + wording (#4918) --- .../notifications/components/EditAlerts.tsx | 4 +- .../components/EditWeeklySummary.tsx | 4 +- .../settings/(account)/profile/page.tsx | 10 +- apps/web/modules/ee/contacts/page.tsx | 2 +- .../web/modules/ee/contacts/segments/page.tsx | 2 +- apps/web/modules/ee/languages/loading.tsx | 2 +- apps/web/modules/ee/languages/page.tsx | 30 ++-- .../components/edit-language.tsx | 157 +++++++++++------- .../components/multi-language-card.tsx | 4 +- .../ee/teams/project-teams/loading.tsx | 36 ++++ .../modules/ee/teams/project-teams/page.tsx | 16 +- .../teams/team-list/components/teams-view.tsx | 2 +- .../email-customization-settings.tsx | 2 +- .../components/branding-settings-card.tsx | 2 +- .../(setup)/app-connection/loading.tsx | 2 +- .../settings/(setup)/app-connection/page.tsx | 16 +- .../projects/settings/api-keys/loading.tsx | 2 +- .../projects/settings/api-keys/page.tsx | 16 +- .../components/project-config-navigation.tsx | 7 +- .../projects/settings/general/loading.tsx | 2 +- .../projects/settings/general/page.tsx | 15 +- apps/web/modules/projects/settings/layout.tsx | 2 +- .../projects/settings/look/loading.tsx | 2 +- .../modules/projects/settings/look/page.tsx | 18 +- .../projects/settings/tags/loading.tsx | 2 +- .../modules/projects/settings/tags/page.tsx | 16 +- .../components/targeting-locked-card.tsx | 2 +- .../ui/components/icons/slack-icon.tsx | 4 +- .../components/secondary-navigation/index.tsx | 127 +++++++------- packages/lib/messages/de-DE.json | 7 +- packages/lib/messages/en-US.json | 11 +- packages/lib/messages/fr-FR.json | 7 +- packages/lib/messages/pt-BR.json | 9 +- packages/lib/messages/pt-PT.json | 9 +- packages/lib/messages/zh-Hant-TW.json | 7 +- 35 files changed, 282 insertions(+), 274 deletions(-) create mode 100644 apps/web/modules/ee/teams/project-teams/loading.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx index 511bb56b26..4871acbe38 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.tsx @@ -27,7 +27,7 @@ export const EditAlerts = ({ return ( <> {memberships.map((membership) => ( - <> +
@@ -110,7 +110,7 @@ export const EditAlerts = ({

- +
))} ); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx index 95a8bd0804..5f99be8309 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx @@ -18,7 +18,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler return ( <> {memberships.map((membership) => ( - <> +
@@ -52,7 +52,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler

- +
))} ); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx index 012f1ff66b..e812e50b5b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx @@ -9,8 +9,10 @@ import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; import { getTranslate } from "@/tolgee/server"; import { getServerSession } from "next-auth"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service"; -import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; +import { + getOrganizationByEnvironmentId, + getOrganizationsWhereUserIsSingleOwner, +} from "@formbricks/lib/organization/service"; import { getUser } from "@formbricks/lib/user/service"; import { SettingsCard } from "../../components/SettingsCard"; import { DeleteAccount } from "./components/DeleteAccount"; @@ -71,7 +73,9 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => { description={t("environments.settings.profile.two_factor_authentication_description")} buttons={[ { - text: t("common.start_free_trial"), + text: IS_FORMBRICKS_CLOUD + ? t("common.start_free_trial") + : t("common.request_trial_license"), href: IS_FORMBRICKS_CLOUD ? `/environments/${params.environmentId}/settings/billing` : "https://formbricks.com/upgrade-self-hosting-license", diff --git a/apps/web/modules/ee/contacts/page.tsx b/apps/web/modules/ee/contacts/page.tsx index 8246f7e2ba..f526065ed1 100644 --- a/apps/web/modules/ee/contacts/page.tsx +++ b/apps/web/modules/ee/contacts/page.tsx @@ -95,7 +95,7 @@ export const ContactsPage = async ({ description={t("environments.contacts.unlock_contacts_description")} buttons={[ { - text: t("common.start_free_trial"), + text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"), href: IS_FORMBRICKS_CLOUD ? `/environments/${params.environmentId}/settings/billing` : "https://formbricks.com/upgrade-self-hosting-license", diff --git a/apps/web/modules/ee/contacts/segments/page.tsx b/apps/web/modules/ee/contacts/segments/page.tsx index 590b388e2b..84025e0176 100644 --- a/apps/web/modules/ee/contacts/segments/page.tsx +++ b/apps/web/modules/ee/contacts/segments/page.tsx @@ -100,7 +100,7 @@ export const SegmentsPage = async ({ description={t("environments.segments.unlock_segments_description")} buttons={[ { - text: t("common.start_free_trial"), + text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"), href: IS_FORMBRICKS_CLOUD ? `/environments/${params.environmentId}/settings/billing` : "https://formbricks.com/upgrade-self-hosting-license", diff --git a/apps/web/modules/ee/languages/loading.tsx b/apps/web/modules/ee/languages/loading.tsx index 49376c2fdf..0b91146d0d 100644 --- a/apps/web/modules/ee/languages/loading.tsx +++ b/apps/web/modules/ee/languages/loading.tsx @@ -11,7 +11,7 @@ export const LanguagesLoading = () => { const { t } = useTranslate(); return ( - + - - + + - + ); diff --git a/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx b/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx index 3c79d26a7b..bd84976dbc 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx @@ -4,9 +4,9 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Alert, AlertDescription } from "@/modules/ui/components/alert"; import { Button } from "@/modules/ui/components/button"; import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal"; +import { ModalButton, UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; import { Language } from "@prisma/client"; -import { useTranslate } from "@tolgee/react"; -import { TFnType } from "@tolgee/react"; +import { TFnType, useTranslate } from "@tolgee/react"; import { PlusIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; @@ -26,6 +26,9 @@ interface EditLanguageProps { project: TProject; locale: TUserLocale; isReadOnly: boolean; + isMultiLanguageAllowed: boolean; + environmentId: string; + isFormbricksCloud: boolean; } const checkIfDuplicateExists = (arr: string[]) => { @@ -57,7 +60,7 @@ const validateLanguages = (languages: Language[], t: TFnType) => { return false; } - // Check if the chosen alias matches an ISO identifier of a language that hasn’t been added + // Check if the chosen alias matches an ISO identifier of a language that hasn't been added for (const alias of languageAliases) { if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) { toast.error(t("environments.project.languages.conflict_between_selected_alias_and_another_language"), { @@ -70,7 +73,14 @@ const validateLanguages = (languages: Language[], t: TFnType) => { return true; }; -export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps) { +export function EditLanguage({ + project, + locale, + isReadOnly, + isMultiLanguageAllowed, + environmentId, + isFormbricksCloud, +}: EditLanguageProps) { const { t } = useTranslate(); const [languages, setLanguages] = useState(project.languages); const [isEditing, setIsEditing] = useState(false); @@ -150,6 +160,21 @@ export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps) setIsEditing(false); }; + const buttons: [ModalButton, ModalButton] = [ + { + text: isFormbricksCloud ? t("common.start_free_trial") : t("common.request_trial_license"), + href: isFormbricksCloud + ? `/environments/${environmentId}/settings/billing` + : "https://formbricks.com/upgrade-self-hosting-license", + }, + { + text: t("common.learn_more"), + href: isFormbricksCloud + ? `/environments/${environmentId}/settings/billing` + : "https://formbricks.com/learn-more-self-hosting-license", + }, + ]; + const handleSaveChanges = async () => { if (!validateLanguages(languages, t)) return; await Promise.all( @@ -179,63 +204,75 @@ export function EditLanguage({ project, locale, isReadOnly }: EditLanguageProps) ) : null; return ( -
-
- {languages.length > 0 ? ( - <> - - {languages.map((language, index) => ( - handleDeleteLanguage(language.id)} - onLanguageChange={(newLanguage: Language) => { - const updatedLanguages = [...languages]; - updatedLanguages[index] = newLanguage; - setLanguages(updatedLanguages); - }} - /> - ))} - - ) : ( -

- {t("environments.project.languages.no_language_found")} -

- )} - -
- { - setIsEditing(true); - }} - onSave={handleSaveChanges} - t={t} - /> - {isReadOnly && ( - - - {t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")} - - + <> + {isMultiLanguageAllowed ? ( +
+
+ {languages.length > 0 ? ( + <> + + {languages.map((language, index) => ( + handleDeleteLanguage(language.id)} + onLanguageChange={(newLanguage: Language) => { + const updatedLanguages = [...languages]; + updatedLanguages[index] = newLanguage; + setLanguages(updatedLanguages); + }} + /> + ))} + + ) : ( +

+ {t("environments.project.languages.no_language_found")} +

+ )} + +
+ { + setIsEditing(true); + }} + onSave={handleSaveChanges} + t={t} + /> + {isReadOnly && ( + + + {t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")} + + + )} + performLanguageDeletion(confirmationModal.languageId)} + open={confirmationModal.isOpen} + setOpen={() => { + setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen })); + }} + text={confirmationModal.text} + title={t("environments.project.languages.remove_language")} + /> +
+ ) : ( + )} - performLanguageDeletion(confirmationModal.languageId)} - open={confirmationModal.isOpen} - setOpen={() => { - setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen })); - }} - text={confirmationModal.text} - title={t("environments.project.languages.remove_language")} - /> -
+ ); } diff --git a/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx b/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx index 942cb4feb1..13718a8549 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/multi-language-card.tsx @@ -230,7 +230,9 @@ export const MultiLanguageCard: FC = ({ description={t("environments.surveys.edit.upgrade_notice_description")} buttons={[ { - text: t("common.start_free_trial"), + text: isFormbricksCloud + ? t("common.start_free_trial") + : t("common.request_trial_license"), href: isFormbricksCloud ? `/environments/${environmentId}/settings/billing` : "https://formbricks.com/docs/self-hosting/license#30-day-trial-license-request", diff --git a/apps/web/modules/ee/teams/project-teams/loading.tsx b/apps/web/modules/ee/teams/project-teams/loading.tsx new file mode 100644 index 0000000000..fe0d0a7a11 --- /dev/null +++ b/apps/web/modules/ee/teams/project-teams/loading.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; +import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; +import { PageHeader } from "@/modules/ui/components/page-header"; +import { useTranslate } from "@tolgee/react"; + +export const TeamsLoading = () => { + const { t } = useTranslate(); + + return ( + + + + +
+
+
+
+
+ {[...Array(3)].map((_, idx) => ( +
+
+
+
+
+
+
+ ))} +
+
+ + ); +}; diff --git a/apps/web/modules/ee/teams/project-teams/page.tsx b/apps/web/modules/ee/teams/project-teams/page.tsx index 4efeaff0b3..a9d269c277 100644 --- a/apps/web/modules/ee/teams/project-teams/page.tsx +++ b/apps/web/modules/ee/teams/project-teams/page.tsx @@ -1,8 +1,4 @@ import { authOptions } from "@/modules/auth/lib/authOptions"; -import { - getMultiLanguagePermission, - getRoleManagementPermission, -} from "@/modules/ee/license-check/lib/utils"; import { AccessView } from "@/modules/ee/teams/project-teams/components/access-view"; import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -37,9 +33,6 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id); const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role); - const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan); - const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); - const teams = await getTeamsByProjectId(project.id); if (!teams) { @@ -50,13 +43,8 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str return ( - - + + diff --git a/apps/web/modules/ee/teams/team-list/components/teams-view.tsx b/apps/web/modules/ee/teams/team-list/components/teams-view.tsx index 9c1c9b994c..e62af40504 100644 --- a/apps/web/modules/ee/teams/team-list/components/teams-view.tsx +++ b/apps/web/modules/ee/teams/team-list/components/teams-view.tsx @@ -37,7 +37,7 @@ export const TeamsView = async ({ const buttons: [ModalButton, ModalButton] = [ { - text: t("common.start_free_trial"), + text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"), href: IS_FORMBRICKS_CLOUD ? `/environments/${environmentId}/settings/billing` : "https://formbricks.com/docs/self-hosting/license#30-day-trial-license-request", diff --git a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx index c97f0001a4..60b1fc9e03 100644 --- a/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx +++ b/apps/web/modules/ee/whitelabel/email-customization/components/email-customization-settings.tsx @@ -162,7 +162,7 @@ export const EmailCustomizationSettings = ({ const buttons: [ModalButton, ModalButton] = [ { - text: t("common.start_free_trial"), + text: isFormbricksCloud ? t("common.start_free_trial") : t("common.request_trial_license"), href: isFormbricksCloud ? `/environments/${environmentId}/settings/billing` : "https://formbricks.com/upgrade-self-hosting-license", diff --git a/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx b/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx index 5b83bf313f..54238c1aa7 100644 --- a/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx +++ b/apps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsx @@ -23,7 +23,7 @@ export const BrandingSettingsCard = async ({ const buttons: [ModalButton, ModalButton] = [ { - text: t("common.start_free_trial"), + text: IS_FORMBRICKS_CLOUD ? t("common.start_free_trial") : t("common.request_trial_license"), href: IS_FORMBRICKS_CLOUD ? `/environments/${environmentId}/settings/billing` : "https://formbricks.com/upgrade-self-hosting-license", diff --git a/apps/web/modules/projects/settings/(setup)/app-connection/loading.tsx b/apps/web/modules/projects/settings/(setup)/app-connection/loading.tsx index b70e2bac74..3dd0c88e65 100644 --- a/apps/web/modules/projects/settings/(setup)/app-connection/loading.tsx +++ b/apps/web/modules/projects/settings/(setup)/app-connection/loading.tsx @@ -35,7 +35,7 @@ export const AppConnectionLoading = () => { return ( - +
diff --git a/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx b/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx index 037d7dbfca..4553295749 100644 --- a/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx +++ b/apps/web/modules/projects/settings/(setup)/app-connection/page.tsx @@ -1,9 +1,5 @@ import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; -import { - getMultiLanguagePermission, - getRoleManagementPermission, -} from "@/modules/ee/license-check/lib/utils"; import { EnvironmentIdField } from "@/modules/projects/settings/(setup)/components/environment-id-field"; import { SetupInstructions } from "@/modules/projects/settings/(setup)/components/setup-instructions"; import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; @@ -31,18 +27,10 @@ export const AppConnectionPage = async (props) => { throw new Error(t("common.organization_not_found")); } - const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan); - const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); - return ( - - + +
diff --git a/apps/web/modules/projects/settings/api-keys/loading.tsx b/apps/web/modules/projects/settings/api-keys/loading.tsx index a712750d66..1ffa986a7d 100644 --- a/apps/web/modules/projects/settings/api-keys/loading.tsx +++ b/apps/web/modules/projects/settings/api-keys/loading.tsx @@ -42,7 +42,7 @@ export const APIKeysLoading = () => { const { t } = useTranslate(); return ( - +
diff --git a/apps/web/modules/projects/settings/api-keys/page.tsx b/apps/web/modules/projects/settings/api-keys/page.tsx index 39f94366ca..43599d983e 100644 --- a/apps/web/modules/projects/settings/api-keys/page.tsx +++ b/apps/web/modules/projects/settings/api-keys/page.tsx @@ -1,9 +1,5 @@ import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; import { authOptions } from "@/modules/auth/lib/authOptions"; -import { - getMultiLanguagePermission, - getRoleManagementPermission, -} from "@/modules/ee/license-check/lib/utils"; import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; @@ -53,18 +49,10 @@ export const APIKeysPage = async (props) => { const isReadOnly = isMember && !hasManageAccess; - const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan); - const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); - return ( - - + + {environment.type === "development" ? ( diff --git a/apps/web/modules/projects/settings/components/project-config-navigation.tsx b/apps/web/modules/projects/settings/components/project-config-navigation.tsx index 0dde0d57f3..a173c1b7c2 100644 --- a/apps/web/modules/projects/settings/components/project-config-navigation.tsx +++ b/apps/web/modules/projects/settings/components/project-config-navigation.tsx @@ -8,17 +8,13 @@ import { usePathname } from "next/navigation"; interface ProjectConfigNavigationProps { activeId: string; environmentId?: string; - isMultiLanguageAllowed?: boolean; loading?: boolean; - canDoRoleManagement?: boolean; } export const ProjectConfigNavigation = ({ activeId, environmentId, - isMultiLanguageAllowed, loading, - canDoRoleManagement, }: ProjectConfigNavigationProps) => { const { t } = useTranslate(); const pathname = usePathname(); @@ -43,7 +39,6 @@ export const ProjectConfigNavigation = ({ label: t("common.survey_languages"), icon: , href: `/environments/${environmentId}/project/languages`, - hidden: !isMultiLanguageAllowed, current: pathname?.includes("/languages"), }, { @@ -70,8 +65,8 @@ export const ProjectConfigNavigation = ({ { id: "teams", label: t("common.team_access"), + icon: , href: `/environments/${environmentId}/project/teams`, - hidden: !canDoRoleManagement, current: pathname?.includes("/teams"), }, ]; diff --git a/apps/web/modules/projects/settings/general/loading.tsx b/apps/web/modules/projects/settings/general/loading.tsx index 63cb5b8e3d..b765f3a5bd 100644 --- a/apps/web/modules/projects/settings/general/loading.tsx +++ b/apps/web/modules/projects/settings/general/loading.tsx @@ -28,7 +28,7 @@ export const GeneralSettingsLoading = () => { return ( - + {cards.map((card, index) => ( diff --git a/apps/web/modules/projects/settings/general/page.tsx b/apps/web/modules/projects/settings/general/page.tsx index 8f765a9505..d021ccdc19 100644 --- a/apps/web/modules/projects/settings/general/page.tsx +++ b/apps/web/modules/projects/settings/general/page.tsx @@ -1,9 +1,5 @@ import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; import { authOptions } from "@/modules/auth/lib/authOptions"; -import { - getMultiLanguagePermission, - getRoleManagementPermission, -} from "@/modules/ee/license-check/lib/utils"; import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; @@ -51,21 +47,12 @@ export const GeneralSettingsPage = async (props: { params: Promise<{ environment const isReadOnly = isMember && !hasManageAccess; - const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan); - const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); - const isOwnerOrManager = isOwner || isManager; return ( - {/* */} - + { diff --git a/apps/web/modules/projects/settings/look/loading.tsx b/apps/web/modules/projects/settings/look/loading.tsx index e723d5f719..17e5f4db3b 100644 --- a/apps/web/modules/projects/settings/look/loading.tsx +++ b/apps/web/modules/projects/settings/look/loading.tsx @@ -24,7 +24,7 @@ export const ProjectLookSettingsLoading = () => { const { t } = useTranslate(); return ( - + - - + + { const { t } = useTranslate(); return ( - + { const isReadOnly = isMember && !hasManageAccess; - const isMultiLanguageAllowed = await getMultiLanguagePermission(organization.billing.plan); - const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan); - return ( - - + + > = (props) => { fill="none" stroke="currentColor" strokeWidth="2" - stroke-linecap="round" - stroke-linejoin="round" + strokeLinecap="round" + strokeLinejoin="round" {...props}> diff --git a/apps/web/modules/ui/components/secondary-navigation/index.tsx b/apps/web/modules/ui/components/secondary-navigation/index.tsx index 6752ac990f..c1dfa1a7b6 100644 --- a/apps/web/modules/ui/components/secondary-navigation/index.tsx +++ b/apps/web/modules/ui/components/secondary-navigation/index.tsx @@ -16,77 +16,72 @@ interface SecondaryNavbarProps { export const SecondaryNavigation = ({ navigation, activeId, loading, ...props }: SecondaryNavbarProps) => { return (
-
-
); }; diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index fd5daf1a00..554cef3a83 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -310,6 +310,7 @@ "remove": "Entfernen", "reorder_and_hide_columns": "Spalten neu anordnen und ausblenden", "report_survey": "Umfrage melden", + "request_trial_license": "Test-Lizenz anfordern", "reset_to_default": "Auf Standard zurücksetzen", "response": "Antwort", "responses": "Antworten", @@ -1133,7 +1134,9 @@ "resend_invitation_email": "Einladungsemail erneut senden", "share_invite_link": "Einladungslink teilen", "share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:", - "test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet" + "test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet", + "use_multi_language_surveys_with_a_higher_plan": "Nutze mehrsprachige Umfragen mit einem höheren Plan", + "use_multi_language_surveys_with_a_higher_plan_description": "Befrage deine Nutzer in verschiedenen Sprachen." }, "notifications": { "auto_subscribe_to_new_surveys": "Neue Umfragenbenachrichtigungen abonnieren", @@ -1177,7 +1180,7 @@ "remove_image": "Bild entfernen", "save_the_following_backup_codes_in_a_safe_place": "Speichere die folgenden Backup-Codes an einem sicheren Ort.", "scan_the_qr_code_below_with_your_authenticator_app": "Scanne den QR-Code unten mit deiner Authentifizierungs-App.", - "security_description": "Verwalte dein Passwort und andere Sicherheitseinstellungen.", + "security_description": "Verwalte dein Passwort und andere Sicherheitseinstellungen wie Zwei-Faktor-Authentifizierung (2FA).", "two_factor_authentication": "Zwei-Faktor-Authentifizierung", "two_factor_authentication_description": "Füge eine zusätzliche Sicherheitsebene zu deinem Konto hinzu, falls dein Passwort gestohlen wird.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Zwei-Faktor-Authentifizierung aktiviert. Bitte gib den sechsstelligen Code aus deiner Authentifizierungs-App ein.", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index dd3814c425..2809d68ac9 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -296,7 +296,7 @@ "product_not_found": "Product not found", "profile": "Profile", "project": "Project", - "project_configuration": "Project's Configuration", + "project_configuration": "Project Configuration", "project_id": "Project ID", "project_name": "Project Name", "project_not_found": "Project not found", @@ -310,6 +310,7 @@ "remove": "Remove", "reorder_and_hide_columns": "Reorder and hide columns", "report_survey": "Report Survey", + "request_trial_license": "Request trial license", "reset_to_default": "Reset to default", "response": "Response", "responses": "Responses", @@ -346,7 +347,7 @@ "some_files_failed_to_upload": "Some files failed to upload", "something_went_wrong_please_try_again": "Something went wrong. Please try again.", "sort_by": "Sort by", - "start_free_trial": "Start Free Trial", + "start_free_trial": "Start free trial", "status": "Status", "step_by_step_manual": "Step by step manual", "styling": "Styling", @@ -1133,7 +1134,9 @@ "resend_invitation_email": "Resend Invitation Email", "share_invite_link": "Share Invite Link", "share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:", - "test_email_sent_successfully": "Test email sent successfully" + "test_email_sent_successfully": "Test email sent successfully", + "use_multi_language_surveys_with_a_higher_plan": "Use multi-language surveys with a higher plan", + "use_multi_language_surveys_with_a_higher_plan_description": "Survey your users in different languages." }, "notifications": { "auto_subscribe_to_new_surveys": "Auto-subscribe to new surveys", @@ -1177,7 +1180,7 @@ "remove_image": "Remove image", "save_the_following_backup_codes_in_a_safe_place": "Save the following backup codes in a safe place.", "scan_the_qr_code_below_with_your_authenticator_app": "Scan the QR code below with your authenticator app.", - "security_description": "Manage your password and other security settings.", + "security_description": "Manage your password and other security settings like two-factor authentication (2FA).", "two_factor_authentication": "Two factor authentication", "two_factor_authentication_description": "Add an extra layer of security to your account in case your password is stolen.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Two-factor authentication enabled. Please enter the six-digit code from your authenticator app.", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index caeec0f4de..c02e99bbd4 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -310,6 +310,7 @@ "remove": "Retirer", "reorder_and_hide_columns": "Réorganiser et masquer des colonnes", "report_survey": "Rapport d'enquête", + "request_trial_license": "Demander licence d'essai", "reset_to_default": "Réinitialiser par défaut", "response": "Réponse", "responses": "Réponses", @@ -1133,7 +1134,9 @@ "resend_invitation_email": "Renvoyer l'e-mail d'invitation", "share_invite_link": "Partager le lien d'invitation", "share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :", - "test_email_sent_successfully": "E-mail de test envoyé avec succès" + "test_email_sent_successfully": "E-mail de test envoyé avec succès", + "use_multi_language_surveys_with_a_higher_plan": "Utiliser des sondages multilingues avec un plan supérieur", + "use_multi_language_surveys_with_a_higher_plan_description": "Sondage vos utilisateurs dans différentes langues." }, "notifications": { "auto_subscribe_to_new_surveys": "S'abonner automatiquement aux nouveaux sondages", @@ -1177,7 +1180,7 @@ "remove_image": "Supprimer l'image", "save_the_following_backup_codes_in_a_safe_place": "Enregistrez les codes de sauvegarde suivants dans un endroit sûr.", "scan_the_qr_code_below_with_your_authenticator_app": "Scannez le code QR ci-dessous avec votre application d'authentification.", - "security_description": "Gérez votre mot de passe et d'autres paramètres de sécurité.", + "security_description": "Gérez votre mot de passe et d'autres paramètres de sécurité comme l'authentification à deux facteurs (2FA).", "two_factor_authentication": "Authentification à deux facteurs", "two_factor_authentication_description": "Ajoutez une couche de sécurité supplémentaire à votre compte au cas où votre mot de passe serait volé.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Authentification à deux facteurs activée. Veuillez entrer le code à six chiffres de votre application d'authentification.", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index dc7f13c148..3ac681a45f 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -310,6 +310,7 @@ "remove": "remover", "reorder_and_hide_columns": "Reordenar e ocultar colunas", "report_survey": "Relatório de Pesquisa", + "request_trial_license": "Solicitar licença de teste", "reset_to_default": "Restaurar para o padrão", "response": "Resposta", "responses": "Respostas", @@ -346,7 +347,7 @@ "some_files_failed_to_upload": "Alguns arquivos falharam ao enviar", "something_went_wrong_please_try_again": "Algo deu errado. Tente novamente.", "sort_by": "Ordenar por", - "start_free_trial": "Iniciar Teste Grátis", + "start_free_trial": "Iniciar teste grátis", "status": "status", "step_by_step_manual": "Manual passo a passo", "styling": "estilização", @@ -1133,7 +1134,9 @@ "resend_invitation_email": "Reenviar E-mail de Convite", "share_invite_link": "Compartilhar Link de Convite", "share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:", - "test_email_sent_successfully": "E-mail de teste enviado com sucesso" + "test_email_sent_successfully": "E-mail de teste enviado com sucesso", + "use_multi_language_surveys_with_a_higher_plan": "Use pesquisas multilingues com um plano superior", + "use_multi_language_surveys_with_a_higher_plan_description": "Pesquise seus usuários em diferentes idiomas." }, "notifications": { "auto_subscribe_to_new_surveys": "Inscrever-se automaticamente em novas pesquisas", @@ -1177,7 +1180,7 @@ "remove_image": "Remover imagem", "save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup em um lugar seguro.", "scan_the_qr_code_below_with_your_authenticator_app": "Escaneie o código QR abaixo com seu app autenticador.", - "security_description": "Gerencie sua senha e outras configurações de segurança.", + "security_description": "Gerencie sua senha e outras configurações de segurança como a autenticação de dois fatores (2FA).", "two_factor_authentication": "Autenticação de dois fatores", "two_factor_authentication_description": "Adicione uma camada extra de segurança à sua conta caso sua senha seja roubada.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Autenticação de dois fatores ativada. Por favor, insira o código de seis dígitos do seu app autenticador.", diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index ee0374a785..c07747368a 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -310,6 +310,7 @@ "remove": "Remover", "reorder_and_hide_columns": "Reordenar e ocultar colunas", "report_survey": "Relatório de Inquérito", + "request_trial_license": "Solicitar licença de teste", "reset_to_default": "Repor para o padrão", "response": "Resposta", "responses": "Respostas", @@ -346,7 +347,7 @@ "some_files_failed_to_upload": "Alguns ficheiros falharam ao carregar", "something_went_wrong_please_try_again": "Algo correu mal. Por favor, tente novamente.", "sort_by": "Ordenar por", - "start_free_trial": "Iniciar Teste Grátis", + "start_free_trial": "Iniciar teste grátis", "status": "Estado", "step_by_step_manual": "Manual passo a passo", "styling": "Estilo", @@ -1133,7 +1134,9 @@ "resend_invitation_email": "Reenviar Email de Convite", "share_invite_link": "Partilhar Link de Convite", "share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:", - "test_email_sent_successfully": "Email de teste enviado com sucesso" + "test_email_sent_successfully": "Email de teste enviado com sucesso", + "use_multi_language_surveys_with_a_higher_plan": "Use inquéritos multilingues com um plano superior", + "use_multi_language_surveys_with_a_higher_plan_description": "Inquira os seus utilizadores em diferentes idiomas." }, "notifications": { "auto_subscribe_to_new_surveys": "Subscrever automaticamente a novos inquéritos", @@ -1177,7 +1180,7 @@ "remove_image": "Remover imagem", "save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup num local seguro.", "scan_the_qr_code_below_with_your_authenticator_app": "Digitalize o código QR abaixo com a sua aplicação de autenticação.", - "security_description": "Gerir a sua palavra-passe e outras definições de segurança.", + "security_description": "Gerir a sua palavra-passe e outras definições de segurança como a autenticação de dois fatores (2FA).", "two_factor_authentication": "Autenticação de dois fatores", "two_factor_authentication_description": "Adicione uma camada extra de segurança à sua conta caso a sua palavra-passe seja roubada.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Autenticação de dois fatores ativada. Introduza o código de seis dígitos da sua aplicação de autenticação.", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index 241651e20f..1107816d21 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -310,6 +310,7 @@ "remove": "移除", "reorder_and_hide_columns": "重新排序和隱藏欄位", "report_survey": "報告問卷", + "request_trial_license": "申請試用授權", "reset_to_default": "重設為預設值", "response": "回應", "responses": "回應", @@ -1133,7 +1134,9 @@ "resend_invitation_email": "重新發送邀請電子郵件", "share_invite_link": "分享邀請連結", "share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:", - "test_email_sent_successfully": "測試電子郵件已成功發送" + "test_email_sent_successfully": "測試電子郵件已成功發送", + "use_multi_language_surveys_with_a_higher_plan": "使用多語言問卷與更高方案", + "use_multi_language_surveys_with_a_higher_plan_description": "用不同語言調查您的用戶。" }, "notifications": { "auto_subscribe_to_new_surveys": "自動訂閱新問卷", @@ -1177,7 +1180,7 @@ "remove_image": "移除圖片", "save_the_following_backup_codes_in_a_safe_place": "將下列備份碼儲存在安全的地方。", "scan_the_qr_code_below_with_your_authenticator_app": "使用您的驗證器應用程式掃描下方的 QR 碼。", - "security_description": "管理您的密碼和其他安全性設定。", + "security_description": "管理您的密碼和其他安全性設定,例如雙重驗證 (2FA)。", "two_factor_authentication": "雙重驗證", "two_factor_authentication_description": "在您的密碼被盜時,為您的帳戶新增額外的安全層。", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "已啟用雙重驗證。請輸入您驗證器應用程式中的六位數程式碼。", From 1529f5d478d11cc2d93e8d4e64aaacda4e45ba1e Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:27:15 +0530 Subject: [PATCH 033/411] fix: click outside is working even if the placement is not center (#4925) --- packages/js-core/src/lib/widget.ts | 1 + packages/lib/messages/de-DE.json | 9 --------- packages/lib/messages/en-US.json | 9 --------- packages/lib/messages/fr-FR.json | 9 --------- packages/lib/messages/pt-BR.json | 9 --------- packages/lib/messages/pt-PT.json | 9 --------- packages/lib/messages/zh-Hant-TW.json | 9 --------- .../surveys/src/components/general/render-survey.tsx | 1 + .../surveys/src/components/wrappers/survey-container.tsx | 6 ++++-- packages/surveys/src/index.ts | 2 +- 10 files changed, 7 insertions(+), 57 deletions(-) diff --git a/packages/js-core/src/lib/widget.ts b/packages/js-core/src/lib/widget.ts index c272a17682..0a01dc67e0 100644 --- a/packages/js-core/src/lib/widget.ts +++ b/packages/js-core/src/lib/widget.ts @@ -74,6 +74,7 @@ const renderWidget = async ( setIsSurveyRunning(true); return; } + languageCode = displayLanguage; } diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index 554cef3a83..931a4aa79d 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -1751,9 +1751,6 @@ "how_to_create_a_panel_step_3_description": "Richte in deiner Formbricks-Umfrage versteckte Felder ein, um nachzuverfolgen, welcher Teilnehmer welche Antwort gegeben hat.", "how_to_create_a_panel_step_4": "Schritt 4: Starte deine Studie", "how_to_create_a_panel_step_4_description": "Sobald alles eingerichtet ist, kannst Du deine Studie starten. Innerhalb weniger Stunden wirst Du die ersten Antworten erhalten.", - "how_to_embed_a_survey_on_your_react_native_app": "Wie man eine Umfrage in deine React Native App einbettet", - "how_to_embed_a_survey_on_your_web_app": "Wie man eine Umfrage in seine App einbettet", - "identify_users_and_set_attributes": "Benutzer identifizieren und Attribute festlegen", "impressions": "Eindrücke", "impressions_tooltip": "Anzahl der Aufrufe der Umfrage.", "includes_all": "Beinhaltet alles", @@ -1768,7 +1765,6 @@ "last_month": "Letztes Monat", "last_quarter": "Letztes Quartal", "last_year": "Letztes Jahr", - "learn_how_to": "Lerne, wie man", "link_to_public_results_copied": "Link zu öffentlichen Ergebnissen kopiert", "make_sure_the_survey_type_is_set_to": "Stelle sicher, dass der Umfragetyp richtig eingestellt ist", "mobile_app": "Mobile App", @@ -1783,7 +1779,6 @@ "send_preview": "Vorschau senden", "send_to_panel": "An das Panel senden", "setup_instructions": "Einrichtung", - "setup_instructions_for_react_native_apps": "Einrichtung für React Native Apps", "setup_integrations": "Integrationen einrichten", "share_results": "Ergebnisse teilen", "share_the_link": "Teile den Link", @@ -1802,10 +1797,7 @@ "this_quarter": "Dieses Quartal", "this_year": "Dieses Jahr", "time_to_complete": "Zeit zur Fertigstellung", - "to_connect_your_app_with_formbricks": "um deine App mit Formbricks zu verbinden", - "to_connect_your_web_app_with_formbricks": "um deine Web-App mit Formbricks zu verbinden", "to_connect_your_website_with_formbricks": "deine Website mit Formbricks zu verbinden", - "to_run_highly_targeted_surveys": "granular zielgerichtete Umfragen durchführen", "ttc_tooltip": "Durchschnittliche Zeit bis zum Abschluss der Umfrage.", "unknown_question_type": "Unbekannter Fragetyp", "unpublish_from_web": "Aus dem Web entfernen", @@ -1815,7 +1807,6 @@ "view_site": "Seite ansehen", "waiting_for_response": "Warte auf eine Antwort \uD83E\uDDD8‍♂️", "web_app": "Web-App", - "were_working_on_sdks_for_flutter_swift_and_kotlin": "Wir arbeiten an SDKs für Flutter, Swift und Kotlin.", "what_is_a_panel": "Was ist ein Panel?", "what_is_a_panel_answer": "Ein Panel ist eine Gruppe von Teilnehmern, die basierend auf Merkmalen wie Alter, Beruf, Geschlecht usw. ausgewählt werden.", "what_is_prolific": "Was ist Prolific?", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 2809d68ac9..2ad2810fd2 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -1751,9 +1751,6 @@ "how_to_create_a_panel_step_3_description": "Set up hidden fields in your Formbricks survey to track which participant provided which answer.", "how_to_create_a_panel_step_4": "Step 4: Launch your study", "how_to_create_a_panel_step_4_description": "Once everything is setup, you can launch your study. Within a few hours you’ll receive the first responses.", - "how_to_embed_a_survey_on_your_react_native_app": "How to embed a survey on your React Native app", - "how_to_embed_a_survey_on_your_web_app": "How to embed a survey on your web app", - "identify_users_and_set_attributes": "identify users and set attributes", "impressions": "Impressions", "impressions_tooltip": "Number of times the survey has been viewed.", "includes_all": "Includes all", @@ -1768,7 +1765,6 @@ "last_month": "Last month", "last_quarter": "Last quarter", "last_year": "Last year", - "learn_how_to": "Learn how to", "link_to_public_results_copied": "Link to public results copied", "make_sure_the_survey_type_is_set_to": "Make sure the survey type is set to", "mobile_app": "Mobile app", @@ -1783,7 +1779,6 @@ "send_preview": "Send preview", "send_to_panel": "Send to panel", "setup_instructions": "Setup instructions", - "setup_instructions_for_react_native_apps": "Setup instructions for React Native apps", "setup_integrations": "Setup integrations", "share_results": "Share results", "share_the_link": "Share the link", @@ -1802,10 +1797,7 @@ "this_quarter": "This quarter", "this_year": "This year", "time_to_complete": "Time to Complete", - "to_connect_your_app_with_formbricks": "to connect your app with Formbricks", - "to_connect_your_web_app_with_formbricks": "to connect your web app with Formbricks", "to_connect_your_website_with_formbricks": "to connect your website with Formbricks", - "to_run_highly_targeted_surveys": "to run highly targeted surveys.", "ttc_tooltip": "Average time to complete the survey.", "unknown_question_type": "Unknown Question Type", "unpublish_from_web": "Unpublish from web", @@ -1815,7 +1807,6 @@ "view_site": "View site", "waiting_for_response": "Waiting for a response \uD83E\uDDD8‍♂️", "web_app": "Web app", - "were_working_on_sdks_for_flutter_swift_and_kotlin": "We're working on SDKs for Flutter, Swift and Kotlin.", "what_is_a_panel": "What is a panel?", "what_is_a_panel_answer": "A panel is a group of participants selected based on characteristics such as age, profession, gender, etc.", "what_is_prolific": "What is Prolific?", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index c02e99bbd4..4083e657dc 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -1751,9 +1751,6 @@ "how_to_create_a_panel_step_3_description": "Configurez des champs cachés dans votre enquête Formbricks pour suivre quel participant a fourni quelle réponse.", "how_to_create_a_panel_step_4": "Étape 4 : Lancez votre étude", "how_to_create_a_panel_step_4_description": "Une fois que tout est configuré, vous pouvez lancer votre étude. Dans quelques heures, vous recevrez les premières réponses.", - "how_to_embed_a_survey_on_your_react_native_app": "Comment intégrer un sondage dans votre application React Native", - "how_to_embed_a_survey_on_your_web_app": "Comment intégrer une enquête dans votre application web", - "identify_users_and_set_attributes": "identifier les utilisateurs et définir des attributs", "impressions": "Impressions", "impressions_tooltip": "Nombre de fois que l'enquête a été consultée.", "includes_all": "Comprend tous", @@ -1768,7 +1765,6 @@ "last_month": "Le mois dernier", "last_quarter": "dernier trimestre", "last_year": "l'année dernière", - "learn_how_to": "Apprenez à", "link_to_public_results_copied": "Lien vers les résultats publics copié", "make_sure_the_survey_type_is_set_to": "Assurez-vous que le type d'enquête est défini sur", "mobile_app": "Application mobile", @@ -1783,7 +1779,6 @@ "send_preview": "Envoyer un aperçu", "send_to_panel": "Envoyer au panneau", "setup_instructions": "Instructions d'installation", - "setup_instructions_for_react_native_apps": "Instructions d'installation pour les applications React Native", "setup_integrations": "Configurer les intégrations", "share_results": "Partager les résultats", "share_the_link": "Partager le lien", @@ -1802,10 +1797,7 @@ "this_quarter": "Ce trimestre", "this_year": "Cette année", "time_to_complete": "Temps à compléter", - "to_connect_your_app_with_formbricks": "pour connecter votre application à Formbricks", - "to_connect_your_web_app_with_formbricks": "pour connecter votre application web à Formbricks", "to_connect_your_website_with_formbricks": "connecter votre site web à Formbricks", - "to_run_highly_targeted_surveys": "réaliser des enquêtes très ciblées.", "ttc_tooltip": "Temps moyen pour compléter l'enquête.", "unknown_question_type": "Type de question inconnu", "unpublish_from_web": "Désactiver la publication sur le web", @@ -1815,7 +1807,6 @@ "view_site": "Voir le site", "waiting_for_response": "En attente d'une réponse \uD83E\uDDD8‍♂️", "web_app": "application web", - "were_working_on_sdks_for_flutter_swift_and_kotlin": "Nous travaillons sur des SDK pour Flutter, Swift et Kotlin.", "what_is_a_panel": "Qu'est-ce qu'un panneau ?", "what_is_a_panel_answer": "Un panel est un groupe de participants sélectionnés en fonction de caractéristiques telles que l'âge, la profession, le sexe, etc.", "what_is_prolific": "Qu'est-ce que Prolific ?", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index 3ac681a45f..bd7198a07b 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -1751,9 +1751,6 @@ "how_to_create_a_panel_step_3_description": "Configure campos ocultos na sua pesquisa do Formbricks para rastrear qual participante forneceu qual resposta.", "how_to_create_a_panel_step_4": "Passo 4: Lançar seu estudo", "how_to_create_a_panel_step_4_description": "Depois que tudo estiver configurado, você pode iniciar seu estudo. Em algumas horas, você vai receber as primeiras respostas.", - "how_to_embed_a_survey_on_your_react_native_app": "Como incorporar uma pesquisa no seu app React Native", - "how_to_embed_a_survey_on_your_web_app": "Como incorporar uma pesquisa no seu app web", - "identify_users_and_set_attributes": "identificar usuários e definir atributos", "impressions": "Impressões", "impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.", "includes_all": "Inclui tudo", @@ -1768,7 +1765,6 @@ "last_month": "Último mês", "last_quarter": "Último trimestre", "last_year": "Último ano", - "learn_how_to": "Aprenda como", "link_to_public_results_copied": "Link pros resultados públicos copiado", "make_sure_the_survey_type_is_set_to": "Certifique-se de que o tipo de pesquisa esteja definido como", "mobile_app": "app de celular", @@ -1783,7 +1779,6 @@ "send_preview": "Enviar prévia", "send_to_panel": "Enviar para o painel", "setup_instructions": "Instruções de configuração", - "setup_instructions_for_react_native_apps": "Instruções de configuração para apps React Native", "setup_integrations": "Configurar integrações", "share_results": "Compartilhar resultados", "share_the_link": "Compartilha o link", @@ -1802,10 +1797,7 @@ "this_quarter": "Este trimestre", "this_year": "Este ano", "time_to_complete": "Tempo para Concluir", - "to_connect_your_app_with_formbricks": "conectar seu app com o Formbricks", - "to_connect_your_web_app_with_formbricks": "conectar seu app web com o Formbricks", "to_connect_your_website_with_formbricks": "conectar seu site com o Formbricks", - "to_run_highly_targeted_surveys": "fazer pesquisas altamente direcionadas.", "ttc_tooltip": "Tempo médio para completar a pesquisa.", "unknown_question_type": "Tipo de pergunta desconhecido", "unpublish_from_web": "Despublicar da web", @@ -1815,7 +1807,6 @@ "view_site": "Ver site", "waiting_for_response": "Aguardando uma resposta \uD83E\uDDD8‍♂️", "web_app": "aplicativo web", - "were_working_on_sdks_for_flutter_swift_and_kotlin": "Estamos trabalhando em SDKs para Flutter, Swift e Kotlin.", "what_is_a_panel": "O que é um painel?", "what_is_a_panel_answer": "Um painel é um grupo de participantes selecionados com base em características como idade, profissão, gênero, etc.", "what_is_prolific": "O que é Prolific?", diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index c07747368a..021251ddcf 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -1751,9 +1751,6 @@ "how_to_create_a_panel_step_3_description": "Configure campos ocultos no seu inquérito Formbricks para rastrear qual participante forneceu qual resposta.", "how_to_create_a_panel_step_4": "Passo 4: Lançar o seu estudo", "how_to_create_a_panel_step_4_description": "Depois de tudo configurado, pode lançar o seu estudo. Dentro de algumas horas, receberá as primeiras respostas.", - "how_to_embed_a_survey_on_your_react_native_app": "Como incorporar um questionário na sua aplicação React Native", - "how_to_embed_a_survey_on_your_web_app": "Como incorporar um questionário na sua aplicação web", - "identify_users_and_set_attributes": "identificar utilizadores e definir atributos", "impressions": "Impressões", "impressions_tooltip": "Número de vezes que o inquérito foi visualizado.", "includes_all": "Inclui tudo", @@ -1768,7 +1765,6 @@ "last_month": "Último mês", "last_quarter": "Último trimestre", "last_year": "Ano passado", - "learn_how_to": "Saiba como", "link_to_public_results_copied": "Link para resultados públicos copiado", "make_sure_the_survey_type_is_set_to": "Certifique-se de que o tipo de inquérito está definido para", "mobile_app": "Aplicação móvel", @@ -1783,7 +1779,6 @@ "send_preview": "Enviar pré-visualização", "send_to_panel": "Enviar para painel", "setup_instructions": "Instruções de configuração", - "setup_instructions_for_react_native_apps": "Instruções de configuração para aplicações React Native", "setup_integrations": "Configurar integrações", "share_results": "Partilhar resultados", "share_the_link": "Partilhar o link", @@ -1802,10 +1797,7 @@ "this_quarter": "Este trimestre", "this_year": "Este ano", "time_to_complete": "Tempo para Concluir", - "to_connect_your_app_with_formbricks": "para ligar a sua aplicação ao Formbricks", - "to_connect_your_web_app_with_formbricks": "para ligar a sua aplicação web ao Formbricks", "to_connect_your_website_with_formbricks": "para ligar o seu website ao Formbricks", - "to_run_highly_targeted_surveys": "para realizar inquéritos altamente direcionados.", "ttc_tooltip": "Tempo médio para concluir o inquérito.", "unknown_question_type": "Tipo de Pergunta Desconhecido", "unpublish_from_web": "Despublicar da web", @@ -1815,7 +1807,6 @@ "view_site": "Ver site", "waiting_for_response": "A aguardar uma resposta \uD83E\uDDD8‍♂️", "web_app": "Aplicação web", - "were_working_on_sdks_for_flutter_swift_and_kotlin": "Estamos a trabalhar em SDKs para Flutter, Swift e Kotlin.", "what_is_a_panel": "O que é um painel?", "what_is_a_panel_answer": "Um painel é um grupo de participantes selecionados com base em características como idade, profissão, género, etc.", "what_is_prolific": "O que é o Prolific?", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index 1107816d21..a9372d9c14 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -1751,9 +1751,6 @@ "how_to_create_a_panel_step_3_description": "在您的 Formbricks 問卷中設定隱藏欄位,以追蹤哪個參與者提供了哪個答案。", "how_to_create_a_panel_step_4": "步驟 4:啟動您的研究", "how_to_create_a_panel_step_4_description": "設定完成後,您可以啟動您的研究。在幾個小時內,您就會收到第一個回應。", - "how_to_embed_a_survey_on_your_react_native_app": "如何在您的 React Native 應用程式中嵌入問卷", - "how_to_embed_a_survey_on_your_web_app": "如何在您的 Web 應用程式中嵌入問卷", - "identify_users_and_set_attributes": "識別使用者並設定屬性", "impressions": "曝光數", "impressions_tooltip": "問卷已檢視的次數。", "includes_all": "包含全部", @@ -1768,7 +1765,6 @@ "last_month": "上個月", "last_quarter": "上一季", "last_year": "去年", - "learn_how_to": "瞭解如何", "link_to_public_results_copied": "已複製公開結果的連結", "make_sure_the_survey_type_is_set_to": "請確保問卷類型設定為", "mobile_app": "行動應用程式", @@ -1783,7 +1779,6 @@ "send_preview": "發送預覽", "send_to_panel": "發送到小組", "setup_instructions": "設定說明", - "setup_instructions_for_react_native_apps": "React Native 應用程式的設定說明", "setup_integrations": "設定整合", "share_results": "分享結果", "share_the_link": "分享連結", @@ -1802,10 +1797,7 @@ "this_quarter": "本季", "this_year": "今年", "time_to_complete": "完成時間", - "to_connect_your_app_with_formbricks": "以將您的應用程式與 Formbricks 連線", - "to_connect_your_web_app_with_formbricks": "以將您的 Web 應用程式與 Formbricks 連線", "to_connect_your_website_with_formbricks": "以將您的網站與 Formbricks 連線", - "to_run_highly_targeted_surveys": "以執行高度目標化的問卷。", "ttc_tooltip": "完成問卷的平均時間。", "unknown_question_type": "未知的問題類型", "unpublish_from_web": "從網站取消發布", @@ -1815,7 +1807,6 @@ "view_site": "檢視網站", "waiting_for_response": "正在等待回應 \uD83E\uDDD8‍♂️", "web_app": "Web 應用程式", - "were_working_on_sdks_for_flutter_swift_and_kotlin": "我們正在開發適用於 Flutter、Swift 和 Kotlin 的 SDK。", "what_is_a_panel": "什麼是小組?", "what_is_a_panel_answer": "小組是一組根據年齡、職業、性別等特徵選取的參與者。", "what_is_prolific": "什麼是 Prolific?", diff --git a/packages/surveys/src/components/general/render-survey.tsx b/packages/surveys/src/components/general/render-survey.tsx index cb3c68196f..737fafd8fb 100644 --- a/packages/surveys/src/components/general/render-survey.tsx +++ b/packages/surveys/src/components/general/render-survey.tsx @@ -26,6 +26,7 @@ export function RenderSurvey(props: SurveyContainerProps) { {/* @ts-expect-error -- TODO: fix this */} { props.onFinished?.(); diff --git a/packages/surveys/src/components/wrappers/survey-container.tsx b/packages/surveys/src/components/wrappers/survey-container.tsx index 6096394cb6..8f201a413f 100644 --- a/packages/surveys/src/components/wrappers/survey-container.tsx +++ b/packages/surveys/src/components/wrappers/survey-container.tsx @@ -32,7 +32,8 @@ export function SurveyContainer({ }, [isOpen]); useEffect(() => { - if (!isCenter && !isModal) return; + if (!isModal) return; + if (!isCenter) return; const handleClickOutside = (e: MouseEvent) => { if ( @@ -49,7 +50,7 @@ export function SurveyContainer({ return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, [show, clickOutside, onClose, isCenter]); + }, [show, clickOutside, onClose, isCenter, isModal]); const getPlacementStyle = (placement: TPlacement): string => { switch (placement) { @@ -77,6 +78,7 @@ export function SurveyContainer({
); } + return (
{ throw new Error(`renderSurvey: Element with id ${containerId} not found.`); } - const { placement, darkOverlay, onClose, ...surveyInlineProps } = props; + const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props; render(h(RenderSurvey, surveyInlineProps), element); } else { From e1140ac43659a3be13e94d749110db79a6edf3a0 Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Wed, 12 Mar 2025 06:00:52 -0700 Subject: [PATCH 034/411] fix: update instructions and docs link (#4921) Co-authored-by: Dhruwang --- .../components/shareEmbedModal/AppTab.tsx | 79 +------------------ .../components/shareEmbedModal/EmbedView.tsx | 2 +- .../shareEmbedModal/MobileAppTab.tsx | 25 ++++++ .../shareEmbedModal/PanelInfoView.tsx | 6 +- .../components/shareEmbedModal/WebAppTab.tsx | 25 ++++++ .../link-surveys/market-research-panel.mdx | 9 +-- packages/lib/messages/de-DE.json | 4 + packages/lib/messages/en-US.json | 4 + packages/lib/messages/fr-FR.json | 4 + packages/lib/messages/pt-BR.json | 4 + packages/lib/messages/pt-PT.json | 4 + packages/lib/messages/zh-Hant-TW.json | 4 + 12 files changed, 85 insertions(+), 85 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx index 930919f8d4..3d72b38aef 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/AppTab.tsx @@ -1,11 +1,12 @@ "use client"; +import { MobileAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab"; +import { WebAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab"; import { OptionsSwitch } from "@/modules/ui/components/options-switch"; import { useTranslate } from "@tolgee/react"; -import Link from "next/link"; import { useState } from "react"; -export const AppTab = ({ environmentId }) => { +export const AppTab = () => { const { t } = useTranslate(); const [selectedTab, setSelectedTab] = useState("webapp"); @@ -20,79 +21,7 @@ export const AppTab = ({ environmentId }) => { handleOptionChange={(value) => setSelectedTab(value)} /> -
- {selectedTab === "webapp" ? : } -
-
- ); -}; - -const MobileAppTab = () => { - const { t } = useTranslate(); - return ( -
-

- {t("environments.surveys.summary.how_to_embed_a_survey_on_your_react_native_app")} -

-
    -
  1. - {t("common.follow_these")}{" "} - - {t("environments.surveys.summary.setup_instructions_for_react_native_apps")} - {" "} - {t("environments.surveys.summary.to_connect_your_app_with_formbricks")} -
  2. -
-
- {t("environments.surveys.summary.were_working_on_sdks_for_flutter_swift_and_kotlin")} -
-
- ); -}; - -const WebAppTab = ({ environmentId }) => { - const { t } = useTranslate(); - return ( -
-

- {t("environments.surveys.summary.how_to_embed_a_survey_on_your_web_app")} -

-
    -
  1. - {t("common.follow_these")}{" "} - - {t("environments.surveys.summary.setup_instructions")} - {" "} - {t("environments.surveys.summary.to_connect_your_web_app_with_formbricks")} -
  2. -
  3. - {t("environments.surveys.summary.learn_how_to")}{" "} - - {t("environments.surveys.summary.identify_users_and_set_attributes")} - {" "} - {t("environments.surveys.summary.to_run_highly_targeted_surveys")}. -
  4. -
  5. - {t("environments.surveys.summary.make_sure_the_survey_type_is_set_to")}{" "} - {t("common.app_survey")} -
  6. -
  7. {t("environments.surveys.summary.define_when_and_where_the_survey_should_pop_up")}
  8. -
-
- -
+
{selectedTab === "webapp" ? : }
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx index 05a3beb876..26a3c9972b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/EmbedView.tsx @@ -88,7 +88,7 @@ export const EmbedView = ({ locale={locale} /> ) : activeId === "app" ? ( - + ) : null}
{tabs.slice(0, 2).map((tab) => ( diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab.tsx new file mode 100644 index 0000000000..fd3fb6b666 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; +import { Button } from "@/modules/ui/components/button"; +import { useTranslate } from "@tolgee/react"; +import Link from "next/link"; + +export const MobileAppTab = () => { + const { t } = useTranslate(); + return ( + + {t("environments.surveys.summary.quickstart_mobile_apps")} + + {t("environments.surveys.summary.quickstart_mobile_apps_description")} + + + + ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/PanelInfoView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/PanelInfoView.tsx index c1c4de6c53..ca9cad500f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/PanelInfoView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/PanelInfoView.tsx @@ -85,8 +85,10 @@ export const PanelInfoView = ({ disableBack, handleInitialPageButton }: PanelInf

diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab.tsx new file mode 100644 index 0000000000..28bfaac59b --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; +import { Button } from "@/modules/ui/components/button"; +import { useTranslate } from "@tolgee/react"; +import Link from "next/link"; + +export const WebAppTab = () => { + const { t } = useTranslate(); + return ( + + {t("environments.surveys.summary.quickstart_web_apps")} + + {t("environments.surveys.summary.quickstart_web_apps_description")} + + + + ); +}; diff --git a/docs/xm-and-surveys/surveys/link-surveys/market-research-panel.mdx b/docs/xm-and-surveys/surveys/link-surveys/market-research-panel.mdx index 6ec3ee3924..4bc2222ea5 100644 --- a/docs/xm-and-surveys/surveys/link-surveys/market-research-panel.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/market-research-panel.mdx @@ -1,16 +1,11 @@ --- -title: "Market Research Panel" +title: "Market Research Panel with Prolific" +sidebarTitle: "Market Research Panel" description: "Formbricks surveys can be integrated with Prolifics participant panel easily. This tutorial walks you through the steps on how to access a pool of over 200.000 participants for your research." icon: "users" --- -# Creating a Research Panel with Prolific - -You need a lot of research participants that match your target audience fast? - -Formbricks integrates well with Prolific. Prolific provides a pool of over 200.000 research participants you can choose from. Run market research with Formbricks within hours, not days. - Prolific is a paid service. You need to fund your account to access the pool of participants. The cost depends on the number of participants you want to diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index 931a4aa79d..1571546136 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -1775,6 +1775,10 @@ "publish_to_web": "Im Web veröffentlichen", "publish_to_web_warning": "Du bist dabei, diese Umfrageergebnisse öffentlich zugänglich zu machen.", "publish_to_web_warning_description": "Deine Umfrageergebnisse werden öffentlich sein. Jeder außerhalb deiner Organisation kann darauf zugreifen, wenn er den Link hat.", + "quickstart_mobile_apps": "Schnellstart: Mobile-Apps", + "quickstart_mobile_apps_description": "Um mit Umfragen in mobilen Apps zu beginnen, folge bitte der Schnellstartanleitung:", + "quickstart_web_apps": "Schnellstart: Web-Apps", + "quickstart_web_apps_description": "Bitte folge der Schnellstartanleitung, um loszulegen:", "results_are_public": "Ergebnisse sind öffentlich", "send_preview": "Vorschau senden", "send_to_panel": "An das Panel senden", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 2ad2810fd2..42a18dadc9 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -1775,6 +1775,10 @@ "publish_to_web": "Publish to web", "publish_to_web_warning": "You are about to release these survey results to the public.", "publish_to_web_warning_description": "Your survey results will be public. Anyone outside your organization can access them if they have the link.", + "quickstart_mobile_apps": "Quickstart: Mobile apps", + "quickstart_mobile_apps_description": "To get started with surveys in mobile apps, please follow the Quickstart guide:", + "quickstart_web_apps": "Quickstart: Web apps", + "quickstart_web_apps_description": "Please follow the Quickstart guide to get started:", "results_are_public": "Results are public", "send_preview": "Send preview", "send_to_panel": "Send to panel", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index 4083e657dc..8600e6906e 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -1775,6 +1775,10 @@ "publish_to_web": "Publier sur le web", "publish_to_web_warning": "Vous êtes sur le point de rendre ces résultats d'enquête publics.", "publish_to_web_warning_description": "Les résultats de votre enquête seront publics. Toute personne en dehors de votre organisation pourra y accéder si elle a le lien.", + "quickstart_mobile_apps": "Démarrage rapide : Applications mobiles", + "quickstart_mobile_apps_description": "Pour commencer avec les enquêtes dans les applications mobiles, veuillez suivre le guide de démarrage rapide :", + "quickstart_web_apps": "Démarrage rapide : Applications web", + "quickstart_web_apps_description": "Veuillez suivre le guide de démarrage rapide pour commencer :", "results_are_public": "Les résultats sont publics.", "send_preview": "Envoyer un aperçu", "send_to_panel": "Envoyer au panneau", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index bd7198a07b..ffdf358003 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -1775,6 +1775,10 @@ "publish_to_web": "Publicar na web", "publish_to_web_warning": "Você está prestes a divulgar esses resultados da pesquisa para o público.", "publish_to_web_warning_description": "Os resultados da sua pesquisa serão públicos. Qualquer pessoa fora da sua organização pode acessá-los se tiver o link.", + "quickstart_mobile_apps": "Início rápido: Aplicativos móveis", + "quickstart_mobile_apps_description": "Para começar com pesquisas em aplicativos móveis, por favor, siga o guia de início rápido:", + "quickstart_web_apps": "Início rápido: Aplicativos web", + "quickstart_web_apps_description": "Por favor, siga o guia de início rápido para começar:", "results_are_public": "Os resultados são públicos", "send_preview": "Enviar prévia", "send_to_panel": "Enviar para o painel", diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index 021251ddcf..fa8cdfd1dd 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -1775,6 +1775,10 @@ "publish_to_web": "Publicar na web", "publish_to_web_warning": "Está prestes a divulgar estes resultados do inquérito ao público.", "publish_to_web_warning_description": "Os resultados do seu inquérito serão públicos. Qualquer pessoa fora da sua organização pode aceder a eles se tiver o link.", + "quickstart_mobile_apps": "Início rápido: Aplicações móveis", + "quickstart_mobile_apps_description": "Para começar com inquéritos em aplicações móveis, por favor, siga o guia de início rápido:", + "quickstart_web_apps": "Início rápido: Aplicações web", + "quickstart_web_apps_description": "Por favor, siga o guia de início rápido para começar:", "results_are_public": "Os resultados são públicos", "send_preview": "Enviar pré-visualização", "send_to_panel": "Enviar para painel", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index a9372d9c14..a9bafe5d0c 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -1775,6 +1775,10 @@ "publish_to_web": "發布至網站", "publish_to_web_warning": "您即將將這些問卷結果發布到公共領域。", "publish_to_web_warning_description": "您的問卷結果將會是公開的。任何組織外的人員都可以存取這些結果(如果他們有連結)。", + "quickstart_mobile_apps": "快速入門:Mobile apps", + "quickstart_mobile_apps_description": "要開始使用行動應用程式中的調查,請按照 Quickstart 指南:", + "quickstart_web_apps": "快速入門:Web apps", + "quickstart_web_apps_description": "請按照 Quickstart 指南開始:", "results_are_public": "結果是公開的", "send_preview": "發送預覽", "send_to_panel": "發送到小組", From fcfe5682daf793c99755ae2ee39b4e51ad5eb613 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:17:43 +0100 Subject: [PATCH 035/411] chore(deps): bump the npm_and_yarn group across 2 directories with 1 update (#4926) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/web/package.json | 2 +- pnpm-lock.yaml | 112 ++++++++++++++++++++++++++++-------------- 2 files changed, 75 insertions(+), 39 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 1d7e569c7e..3030e71856 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -108,7 +108,7 @@ "otplib": "12.0.1", "papaparse": "5.4.1", "posthog-js": "1.200.2", - "prismjs": "1.29.0", + "prismjs": "1.30.0", "react": "19.0.0", "react-colorful": "5.6.1", "react-confetti": "6.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e70f7d77d..b3a0ac2c61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -454,8 +454,8 @@ importers: specifier: 1.200.2 version: 1.200.2 prismjs: - specifier: 1.29.0 - version: 1.29.0 + specifier: 1.30.0 + version: 1.30.0 react: specifier: 19.0.0 version: 19.0.0 @@ -1382,6 +1382,10 @@ packages: '@babel/core': ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + '@babel/generator@7.26.10': + resolution: {integrity: sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.26.5': resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} engines: {node: '>=6.9.0'} @@ -1487,6 +1491,11 @@ packages: resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} engines: {node: '>=6.9.0'} + '@babel/parser@7.26.10': + resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@7.26.7': resolution: {integrity: sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==} engines: {node: '>=6.0.0'} @@ -2140,12 +2149,16 @@ packages: resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.10': + resolution: {integrity: sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.7': resolution: {integrity: sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.26.9': - resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} + '@babel/types@7.26.10': + resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} engines: {node: '>=6.9.0'} '@babel/types@7.26.7': @@ -11359,6 +11372,10 @@ packages: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + proc-log@4.2.0: resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -14738,6 +14755,14 @@ snapshots: eslint-visitor-keys: 2.1.0 semver: 6.3.1 + '@babel/generator@7.26.10': + dependencies: + '@babel/parser': 7.26.10 + '@babel/types': 7.26.10 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + '@babel/generator@7.26.5': dependencies: '@babel/parser': 7.26.7 @@ -14748,8 +14773,8 @@ snapshots: '@babel/generator@7.26.9': dependencies: - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.26.10 + '@babel/types': 7.26.10 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 @@ -14787,7 +14812,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.25.9 '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.0) '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.26.10 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -14812,12 +14837,12 @@ snapshots: '@babel/helper-environment-visitor@7.24.7': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.26.7 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 transitivePeerDependencies: - supports-color @@ -14839,7 +14864,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@babel/helper-plugin-utils@7.26.5': {} @@ -14864,7 +14889,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: '@babel/traverse': 7.26.7 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 transitivePeerDependencies: - supports-color @@ -14878,7 +14903,7 @@ snapshots: dependencies: '@babel/template': 7.26.9 '@babel/traverse': 7.26.7 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 transitivePeerDependencies: - supports-color @@ -14894,19 +14919,23 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/parser@7.26.10': + dependencies: + '@babel/types': 7.26.10 + '@babel/parser@7.26.7': dependencies: '@babel/types': 7.26.7 '@babel/parser@7.26.9': dependencies: - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.26.5 - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color @@ -14933,7 +14962,7 @@ snapshots: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.26.5 - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color @@ -15150,7 +15179,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.26.5 '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color @@ -15317,7 +15346,7 @@ snapshots: '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) '@babel/helper-plugin-utils': 7.26.5 '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.9 + '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color @@ -15621,7 +15650,7 @@ snapshots: dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.26.5 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 esutils: 2.0.3 '@babel/preset-react@7.26.3(@babel/core@7.26.0)': @@ -15669,8 +15698,20 @@ snapshots: '@babel/template@7.26.9': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/parser': 7.26.10 + '@babel/types': 7.26.10 + + '@babel/traverse@7.26.10': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.10 + '@babel/parser': 7.26.10 + '@babel/template': 7.26.9 + '@babel/types': 7.26.10 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color '@babel/traverse@7.26.7': dependencies: @@ -15684,17 +15725,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.26.9': + '@babel/types@7.26.10': dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/template': 7.26.9 - '@babel/types': 7.26.9 - debug: 4.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 '@babel/types@7.26.7': dependencies: @@ -16934,7 +16968,7 @@ snapshots: dependencies: '@lexical/utils': 0.21.0 lexical: 0.21.0 - prismjs: 1.29.0 + prismjs: 1.30.0 '@lexical/devtools-core@0.21.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: @@ -21423,7 +21457,7 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.26.9 - '@babel/types': 7.26.9 + '@babel/types': 7.26.10 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 @@ -24464,7 +24498,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.26.0 - '@babel/parser': 7.26.9 + '@babel/parser': 7.26.10 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -25449,7 +25483,7 @@ snapshots: metro-source-map@0.81.0: dependencies: '@babel/traverse': 7.26.7 - '@babel/traverse--for-generate-function-map': '@babel/traverse@7.26.9' + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.26.10' '@babel/types': 7.26.7 flow-enums-runtime: 0.0.6 invariant: 2.2.4 @@ -25499,7 +25533,7 @@ snapshots: metro-transform-plugins@0.81.0: dependencies: '@babel/core': 7.26.0 - '@babel/generator': 7.26.9 + '@babel/generator': 7.26.10 '@babel/template': 7.26.9 '@babel/traverse': 7.26.7 flow-enums-runtime: 0.0.6 @@ -25530,9 +25564,9 @@ snapshots: metro-transform-worker@0.81.0: dependencies: '@babel/core': 7.26.0 - '@babel/generator': 7.26.9 - '@babel/parser': 7.26.9 - '@babel/types': 7.26.9 + '@babel/generator': 7.26.10 + '@babel/parser': 7.26.10 + '@babel/types': 7.26.10 flow-enums-runtime: 0.0.6 metro: 0.81.0 metro-babel-transformer: 0.81.0 @@ -26920,6 +26954,8 @@ snapshots: prismjs@1.29.0: {} + prismjs@1.30.0: {} + proc-log@4.2.0: {} process-nextick-args@2.0.1: {} From 655f3190830955c3c4e8a554358766baceaecac9 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Wed, 12 Mar 2025 14:39:53 +0100 Subject: [PATCH 036/411] chore: add bug label to new bug issues (#4929) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + packages/lib/messages/de-DE.json | 12 +++++++++--- packages/lib/messages/en-US.json | 14 ++++++++++---- packages/lib/messages/fr-FR.json | 12 +++++++++--- packages/lib/messages/pt-BR.json | 14 ++++++++++---- packages/lib/messages/pt-PT.json | 14 ++++++++++---- packages/lib/messages/zh-Hant-TW.json | 12 +++++++++--- 7 files changed, 58 insertions(+), 21 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 56d63a0cb0..2bf24b01c3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,7 @@ name: Bug report description: "Found a bug? Please fill out the sections below. \U0001F44D" type: bug +labels: ["bug"] body: - type: textarea id: issue-summary diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index 1571546136..68e94aeb50 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -310,7 +310,7 @@ "remove": "Entfernen", "reorder_and_hide_columns": "Spalten neu anordnen und ausblenden", "report_survey": "Umfrage melden", - "request_trial_license": "Test-Lizenz anfordern", + "request_trial_license": "Testlizenz anfordern", "reset_to_default": "Auf Standard zurücksetzen", "response": "Antwort", "responses": "Antworten", @@ -512,6 +512,13 @@ "weekly_summary_create_reminder_notification_body_text": "Wir würden dir gerne eine wöchentliche Zusammenfassung schicken, aber momentan laufen keine Umfragen für {projectName}.", "weekly_summary_email_subject": "{projectName} Nutzer-Insights – Letzte Woche von Formbricks" }, + "environment": { + "settings": { + "general": { + "use_multi_language_surveys_with_a_higher_plan_description": "Befrage deine Nutzer in verschiedenen Sprachen." + } + } + }, "environments": { "actions": { "action_copied_successfully": "Aktion erfolgreich kopiert", @@ -1135,8 +1142,7 @@ "share_invite_link": "Einladungslink teilen", "share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:", "test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet", - "use_multi_language_surveys_with_a_higher_plan": "Nutze mehrsprachige Umfragen mit einem höheren Plan", - "use_multi_language_surveys_with_a_higher_plan_description": "Befrage deine Nutzer in verschiedenen Sprachen." + "use_multi_language_surveys_with_a_higher_plan": "Nutze mehrsprachige Umfragen mit einem höheren Plan" }, "notifications": { "auto_subscribe_to_new_surveys": "Neue Umfragenbenachrichtigungen abonnieren", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 42a18dadc9..40c37de0ef 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -296,7 +296,7 @@ "product_not_found": "Product not found", "profile": "Profile", "project": "Project", - "project_configuration": "Project Configuration", + "project_configuration": "Project's Configuration", "project_id": "Project ID", "project_name": "Project Name", "project_not_found": "Project not found", @@ -347,7 +347,7 @@ "some_files_failed_to_upload": "Some files failed to upload", "something_went_wrong_please_try_again": "Something went wrong. Please try again.", "sort_by": "Sort by", - "start_free_trial": "Start free trial", + "start_free_trial": "Start Free Trial", "status": "Status", "step_by_step_manual": "Step by step manual", "styling": "Styling", @@ -512,6 +512,13 @@ "weekly_summary_create_reminder_notification_body_text": "We'd love to send you a Weekly Summary, but currently there are no surveys running for {projectName}.", "weekly_summary_email_subject": "{projectName} User Insights - Last Week by Formbricks" }, + "environment": { + "settings": { + "general": { + "use_multi_language_surveys_with_a_higher_plan_description": "Survey your users in different languages." + } + } + }, "environments": { "actions": { "action_copied_successfully": "Action copied successfully", @@ -1135,8 +1142,7 @@ "share_invite_link": "Share Invite Link", "share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:", "test_email_sent_successfully": "Test email sent successfully", - "use_multi_language_surveys_with_a_higher_plan": "Use multi-language surveys with a higher plan", - "use_multi_language_surveys_with_a_higher_plan_description": "Survey your users in different languages." + "use_multi_language_surveys_with_a_higher_plan": "Use multi-language surveys with a higher plan" }, "notifications": { "auto_subscribe_to_new_surveys": "Auto-subscribe to new surveys", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index 8600e6906e..0931aa27ea 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -310,7 +310,7 @@ "remove": "Retirer", "reorder_and_hide_columns": "Réorganiser et masquer des colonnes", "report_survey": "Rapport d'enquête", - "request_trial_license": "Demander licence d'essai", + "request_trial_license": "Demander une licence d'essai", "reset_to_default": "Réinitialiser par défaut", "response": "Réponse", "responses": "Réponses", @@ -512,6 +512,13 @@ "weekly_summary_create_reminder_notification_body_text": "Nous aimerions vous envoyer un résumé hebdomadaire, mais actuellement, il n'y a pas d'enquêtes en cours pour {projectName}.", "weekly_summary_email_subject": "Aperçu des utilisateurs de {projectName} – La semaine dernière par Formbricks" }, + "environment": { + "settings": { + "general": { + "use_multi_language_surveys_with_a_higher_plan_description": "Interrogez vos utilisateurs dans différentes langues." + } + } + }, "environments": { "actions": { "action_copied_successfully": "Action copiée avec succès", @@ -1135,8 +1142,7 @@ "share_invite_link": "Partager le lien d'invitation", "share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :", "test_email_sent_successfully": "E-mail de test envoyé avec succès", - "use_multi_language_surveys_with_a_higher_plan": "Utiliser des sondages multilingues avec un plan supérieur", - "use_multi_language_surveys_with_a_higher_plan_description": "Sondage vos utilisateurs dans différentes langues." + "use_multi_language_surveys_with_a_higher_plan": "Utilisez des sondages multilingues avec un plan supérieur" }, "notifications": { "auto_subscribe_to_new_surveys": "S'abonner automatiquement aux nouveaux sondages", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index ffdf358003..8208fa0ded 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -310,7 +310,7 @@ "remove": "remover", "reorder_and_hide_columns": "Reordenar e ocultar colunas", "report_survey": "Relatório de Pesquisa", - "request_trial_license": "Solicitar licença de teste", + "request_trial_license": "Pedir licença de teste", "reset_to_default": "Restaurar para o padrão", "response": "Resposta", "responses": "Respostas", @@ -347,7 +347,7 @@ "some_files_failed_to_upload": "Alguns arquivos falharam ao enviar", "something_went_wrong_please_try_again": "Algo deu errado. Tente novamente.", "sort_by": "Ordenar por", - "start_free_trial": "Iniciar teste grátis", + "start_free_trial": "Iniciar Teste Grátis", "status": "status", "step_by_step_manual": "Manual passo a passo", "styling": "estilização", @@ -512,6 +512,13 @@ "weekly_summary_create_reminder_notification_body_text": "Adoraríamos te enviar um Resumo Semanal, mas no momento não há pesquisas em andamento para {projectName}.", "weekly_summary_email_subject": "Insights de usuários do {projectName} – Semana passada por Formbricks" }, + "environment": { + "settings": { + "general": { + "use_multi_language_surveys_with_a_higher_plan_description": "Pesquise seus usuários em diferentes idiomas." + } + } + }, "environments": { "actions": { "action_copied_successfully": "Ação copiada com sucesso", @@ -1135,8 +1142,7 @@ "share_invite_link": "Compartilhar Link de Convite", "share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:", "test_email_sent_successfully": "E-mail de teste enviado com sucesso", - "use_multi_language_surveys_with_a_higher_plan": "Use pesquisas multilingues com um plano superior", - "use_multi_language_surveys_with_a_higher_plan_description": "Pesquise seus usuários em diferentes idiomas." + "use_multi_language_surveys_with_a_higher_plan": "Use pesquisas multilíngues com um plano superior" }, "notifications": { "auto_subscribe_to_new_surveys": "Inscrever-se automaticamente em novas pesquisas", diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index fa8cdfd1dd..3807598a04 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -347,7 +347,7 @@ "some_files_failed_to_upload": "Alguns ficheiros falharam ao carregar", "something_went_wrong_please_try_again": "Algo correu mal. Por favor, tente novamente.", "sort_by": "Ordenar por", - "start_free_trial": "Iniciar teste grátis", + "start_free_trial": "Iniciar Teste Grátis", "status": "Estado", "step_by_step_manual": "Manual passo a passo", "styling": "Estilo", @@ -512,6 +512,13 @@ "weekly_summary_create_reminder_notification_body_text": "Gostaríamos de lhe enviar um Resumo Semanal, mas de momento não há inquéritos a decorrer para {projectName}.", "weekly_summary_email_subject": "{projectName} Informações do Utilizador - Última Semana por Formbricks" }, + "environment": { + "settings": { + "general": { + "use_multi_language_surveys_with_a_higher_plan_description": "Inquira os seus utilizadores em diferentes idiomas." + } + } + }, "environments": { "actions": { "action_copied_successfully": "Ação copiada com sucesso", @@ -1135,8 +1142,7 @@ "share_invite_link": "Partilhar Link de Convite", "share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:", "test_email_sent_successfully": "Email de teste enviado com sucesso", - "use_multi_language_surveys_with_a_higher_plan": "Use inquéritos multilingues com um plano superior", - "use_multi_language_surveys_with_a_higher_plan_description": "Inquira os seus utilizadores em diferentes idiomas." + "use_multi_language_surveys_with_a_higher_plan": "Use inquéritos multilingues com um plano superior" }, "notifications": { "auto_subscribe_to_new_surveys": "Subscrever automaticamente a novos inquéritos", @@ -1180,7 +1186,7 @@ "remove_image": "Remover imagem", "save_the_following_backup_codes_in_a_safe_place": "Guarde os seguintes códigos de backup num local seguro.", "scan_the_qr_code_below_with_your_authenticator_app": "Digitalize o código QR abaixo com a sua aplicação de autenticação.", - "security_description": "Gerir a sua palavra-passe e outras definições de segurança como a autenticação de dois fatores (2FA).", + "security_description": "Gerir a sua palavra-passe e outras definições de segurança, como a autenticação de dois fatores (2FA).", "two_factor_authentication": "Autenticação de dois fatores", "two_factor_authentication_description": "Adicione uma camada extra de segurança à sua conta caso a sua palavra-passe seja roubada.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Autenticação de dois fatores ativada. Introduza o código de seis dígitos da sua aplicação de autenticação.", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index a9bafe5d0c..060bf6ba62 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -310,7 +310,7 @@ "remove": "移除", "reorder_and_hide_columns": "重新排序和隱藏欄位", "report_survey": "報告問卷", - "request_trial_license": "申請試用授權", + "request_trial_license": "請求試用授權", "reset_to_default": "重設為預設值", "response": "回應", "responses": "回應", @@ -512,6 +512,13 @@ "weekly_summary_create_reminder_notification_body_text": "我們很樂意向您發送每週摘要,但目前 '{'projectName'}' 沒有正在執行的問卷。", "weekly_summary_email_subject": "{projectName} 用戶洞察 - 上週 by Formbricks" }, + "environment": { + "settings": { + "general": { + "use_multi_language_surveys_with_a_higher_plan_description": "用不同語言調查您的用戶。" + } + } + }, "environments": { "actions": { "action_copied_successfully": "操作已成功複製", @@ -1135,8 +1142,7 @@ "share_invite_link": "分享邀請連結", "share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:", "test_email_sent_successfully": "測試電子郵件已成功發送", - "use_multi_language_surveys_with_a_higher_plan": "使用多語言問卷與更高方案", - "use_multi_language_surveys_with_a_higher_plan_description": "用不同語言調查您的用戶。" + "use_multi_language_surveys_with_a_higher_plan": "使用更高等級的方案使用多語言問卷" }, "notifications": { "auto_subscribe_to_new_surveys": "自動訂閱新問卷", From daa7e7b56a75e804a938da6083bab9159a7c7188 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Wed, 12 Mar 2025 19:34:10 +0530 Subject: [PATCH 037/411] fix: Update tolgee.yml (#4928) --- .github/workflows/tolgee.yml | 3 --- packages/lib/messages/de-DE.json | 10 ++-------- packages/lib/messages/en-US.json | 10 ++-------- packages/lib/messages/fr-FR.json | 10 ++-------- packages/lib/messages/pt-BR.json | 10 ++-------- packages/lib/messages/pt-PT.json | 10 ++-------- packages/lib/messages/zh-Hant-TW.json | 10 ++-------- 7 files changed, 12 insertions(+), 51 deletions(-) diff --git a/.github/workflows/tolgee.yml b/.github/workflows/tolgee.yml index 53810a027d..cc7fbcb9da 100644 --- a/.github/workflows/tolgee.yml +++ b/.github/workflows/tolgee.yml @@ -51,7 +51,6 @@ jobs: --filter-tag "draft:${SOURCE_BRANCH}" \ --tag production \ --untag "draft:${SOURCE_BRANCH}" - --verbose - name: Tag unused production keys as Deprecated run: | @@ -59,7 +58,6 @@ jobs: --api-key ${{ secrets.TOLGEE_API_KEY }} \ --filter-not-extracted --filter-tag production \ --tag deprecated --untag production - --verbose - name: Tag unused draft:current-branch keys as Deprecated run: | @@ -67,7 +65,6 @@ jobs: --api-key ${{ secrets.TOLGEE_API_KEY }} \ --filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \ --tag deprecated --untag "draft:${SOURCE_BRANCH}" - --verbose - name: Sync with backup run: | diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index 68e94aeb50..6c2089c784 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -512,13 +512,6 @@ "weekly_summary_create_reminder_notification_body_text": "Wir würden dir gerne eine wöchentliche Zusammenfassung schicken, aber momentan laufen keine Umfragen für {projectName}.", "weekly_summary_email_subject": "{projectName} Nutzer-Insights – Letzte Woche von Formbricks" }, - "environment": { - "settings": { - "general": { - "use_multi_language_surveys_with_a_higher_plan_description": "Befrage deine Nutzer in verschiedenen Sprachen." - } - } - }, "environments": { "actions": { "action_copied_successfully": "Aktion erfolgreich kopiert", @@ -1142,7 +1135,8 @@ "share_invite_link": "Einladungslink teilen", "share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:", "test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet", - "use_multi_language_surveys_with_a_higher_plan": "Nutze mehrsprachige Umfragen mit einem höheren Plan" + "use_multi_language_surveys_with_a_higher_plan": "Nutze mehrsprachige Umfragen mit einem höheren Plan", + "use_multi_language_surveys_with_a_higher_plan_description": "Befrage deine Nutzer in verschiedenen Sprachen." }, "notifications": { "auto_subscribe_to_new_surveys": "Neue Umfragenbenachrichtigungen abonnieren", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 40c37de0ef..0903d4e404 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -512,13 +512,6 @@ "weekly_summary_create_reminder_notification_body_text": "We'd love to send you a Weekly Summary, but currently there are no surveys running for {projectName}.", "weekly_summary_email_subject": "{projectName} User Insights - Last Week by Formbricks" }, - "environment": { - "settings": { - "general": { - "use_multi_language_surveys_with_a_higher_plan_description": "Survey your users in different languages." - } - } - }, "environments": { "actions": { "action_copied_successfully": "Action copied successfully", @@ -1142,7 +1135,8 @@ "share_invite_link": "Share Invite Link", "share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:", "test_email_sent_successfully": "Test email sent successfully", - "use_multi_language_surveys_with_a_higher_plan": "Use multi-language surveys with a higher plan" + "use_multi_language_surveys_with_a_higher_plan": "Use multi-language surveys with a higher plan", + "use_multi_language_surveys_with_a_higher_plan_description": "Survey your users in different languages." }, "notifications": { "auto_subscribe_to_new_surveys": "Auto-subscribe to new surveys", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index 0931aa27ea..cd4ba7ef6b 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -512,13 +512,6 @@ "weekly_summary_create_reminder_notification_body_text": "Nous aimerions vous envoyer un résumé hebdomadaire, mais actuellement, il n'y a pas d'enquêtes en cours pour {projectName}.", "weekly_summary_email_subject": "Aperçu des utilisateurs de {projectName} – La semaine dernière par Formbricks" }, - "environment": { - "settings": { - "general": { - "use_multi_language_surveys_with_a_higher_plan_description": "Interrogez vos utilisateurs dans différentes langues." - } - } - }, "environments": { "actions": { "action_copied_successfully": "Action copiée avec succès", @@ -1142,7 +1135,8 @@ "share_invite_link": "Partager le lien d'invitation", "share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :", "test_email_sent_successfully": "E-mail de test envoyé avec succès", - "use_multi_language_surveys_with_a_higher_plan": "Utilisez des sondages multilingues avec un plan supérieur" + "use_multi_language_surveys_with_a_higher_plan": "Utilisez des sondages multilingues avec un plan supérieur", + "use_multi_language_surveys_with_a_higher_plan_description": "Interrogez vos utilisateurs dans différentes langues." }, "notifications": { "auto_subscribe_to_new_surveys": "S'abonner automatiquement aux nouveaux sondages", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index 8208fa0ded..9a6952aa05 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -512,13 +512,6 @@ "weekly_summary_create_reminder_notification_body_text": "Adoraríamos te enviar um Resumo Semanal, mas no momento não há pesquisas em andamento para {projectName}.", "weekly_summary_email_subject": "Insights de usuários do {projectName} – Semana passada por Formbricks" }, - "environment": { - "settings": { - "general": { - "use_multi_language_surveys_with_a_higher_plan_description": "Pesquise seus usuários em diferentes idiomas." - } - } - }, "environments": { "actions": { "action_copied_successfully": "Ação copiada com sucesso", @@ -1142,7 +1135,8 @@ "share_invite_link": "Compartilhar Link de Convite", "share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:", "test_email_sent_successfully": "E-mail de teste enviado com sucesso", - "use_multi_language_surveys_with_a_higher_plan": "Use pesquisas multilíngues com um plano superior" + "use_multi_language_surveys_with_a_higher_plan": "Use pesquisas multilíngues com um plano superior", + "use_multi_language_surveys_with_a_higher_plan_description": "Pesquise seus usuários em diferentes idiomas." }, "notifications": { "auto_subscribe_to_new_surveys": "Inscrever-se automaticamente em novas pesquisas", diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index 3807598a04..6542cb3e8a 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -512,13 +512,6 @@ "weekly_summary_create_reminder_notification_body_text": "Gostaríamos de lhe enviar um Resumo Semanal, mas de momento não há inquéritos a decorrer para {projectName}.", "weekly_summary_email_subject": "{projectName} Informações do Utilizador - Última Semana por Formbricks" }, - "environment": { - "settings": { - "general": { - "use_multi_language_surveys_with_a_higher_plan_description": "Inquira os seus utilizadores em diferentes idiomas." - } - } - }, "environments": { "actions": { "action_copied_successfully": "Ação copiada com sucesso", @@ -1142,7 +1135,8 @@ "share_invite_link": "Partilhar Link de Convite", "share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:", "test_email_sent_successfully": "Email de teste enviado com sucesso", - "use_multi_language_surveys_with_a_higher_plan": "Use inquéritos multilingues com um plano superior" + "use_multi_language_surveys_with_a_higher_plan": "Use inquéritos multilingues com um plano superior", + "use_multi_language_surveys_with_a_higher_plan_description": "Inquira os seus utilizadores em diferentes idiomas." }, "notifications": { "auto_subscribe_to_new_surveys": "Subscrever automaticamente a novos inquéritos", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index 060bf6ba62..0469670f44 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -512,13 +512,6 @@ "weekly_summary_create_reminder_notification_body_text": "我們很樂意向您發送每週摘要,但目前 '{'projectName'}' 沒有正在執行的問卷。", "weekly_summary_email_subject": "{projectName} 用戶洞察 - 上週 by Formbricks" }, - "environment": { - "settings": { - "general": { - "use_multi_language_surveys_with_a_higher_plan_description": "用不同語言調查您的用戶。" - } - } - }, "environments": { "actions": { "action_copied_successfully": "操作已成功複製", @@ -1142,7 +1135,8 @@ "share_invite_link": "分享邀請連結", "share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:", "test_email_sent_successfully": "測試電子郵件已成功發送", - "use_multi_language_surveys_with_a_higher_plan": "使用更高等級的方案使用多語言問卷" + "use_multi_language_surveys_with_a_higher_plan": "使用更高等級的方案使用多語言問卷", + "use_multi_language_surveys_with_a_higher_plan_description": "用不同語言調查您的用戶。" }, "notifications": { "auto_subscribe_to_new_surveys": "自動訂閱新問卷", From 5d0c435a33febcc73cb7e68a8edc922cd0923f11 Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:51:14 +0530 Subject: [PATCH 038/411] feat: @formbricks/react-native `v2.1.0` release (#4927) --- packages/lib/messages/de-DE.json | 1 - packages/lib/messages/en-US.json | 1 - packages/lib/messages/fr-FR.json | 1 - packages/lib/messages/pt-BR.json | 1 - packages/lib/messages/pt-PT.json | 1 - packages/lib/messages/zh-Hant-TW.json | 1 - packages/react-native/package.json | 2 +- 7 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index 6c2089c784..b82d8c2a15 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -194,7 +194,6 @@ "full_name": "Name", "gathering_responses": "Antworten sammeln", "general": "Allgemein", - "get_started": "Leg los", "go_back": "Geh zurück", "go_to_dashboard": "Zum Dashboard gehen", "hidden": "Versteckt", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 0903d4e404..6f12cb0682 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -194,7 +194,6 @@ "full_name": "Full name", "gathering_responses": "Gathering responses", "general": "General", - "get_started": "Get started", "go_back": "Go Back", "go_to_dashboard": "Go to Dashboard", "hidden": "Hidden", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index cd4ba7ef6b..3c7413c611 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -194,7 +194,6 @@ "full_name": "Nom complet", "gathering_responses": "Collecte des réponses", "general": "Général", - "get_started": "Commencer", "go_back": "Retourner", "go_to_dashboard": "Aller au tableau de bord", "hidden": "Caché", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index 9a6952aa05..0fabba26e2 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -194,7 +194,6 @@ "full_name": "Nome completo", "gathering_responses": "Recolhendo respostas", "general": "geral", - "get_started": "Começar", "go_back": "Voltar", "go_to_dashboard": "Ir para o Painel", "hidden": "Escondido", diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index 6542cb3e8a..afe1fbfe53 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -194,7 +194,6 @@ "full_name": "Nome completo", "gathering_responses": "A recolher respostas", "general": "Geral", - "get_started": "Começar", "go_back": "Voltar", "go_to_dashboard": "Ir para o Painel", "hidden": "Oculto", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index 0469670f44..0b11fe6549 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -194,7 +194,6 @@ "full_name": "全名", "gathering_responses": "收集回應中", "general": "一般", - "get_started": "開始使用", "go_back": "返回", "go_to_dashboard": "前往儀表板", "hidden": "隱藏", diff --git a/packages/react-native/package.json b/packages/react-native/package.json index b8d21db09f..95468ba426 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/react-native", - "version": "2.0.1", + "version": "2.1.0", "license": "MIT", "description": "Formbricks React Native SDK allows you to connect your app to Formbricks, display surveys and trigger events.", "homepage": "https://formbricks.com", From e0f180bf0436cf696bf7500e411f665e62cdef23 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Thu, 13 Mar 2025 00:46:08 +0530 Subject: [PATCH 039/411] feat: open telemetry for prometheus (#4922) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .env.example | 6 +- apps/web/instrumentation-node.ts | 58 ++++++++ apps/web/instrumentation.ts | 27 +--- apps/web/lib/otelSetup.ts | 0 apps/web/next.config.mjs | 9 +- apps/web/package.json | 6 + apps/web/prometheus.yml | 5 + .../configuration/environment-variables.mdx | 2 + packages/lib/env.ts | 4 + pnpm-lock.yaml | 139 ++++++++++++++++-- turbo.json | 4 +- 11 files changed, 221 insertions(+), 39 deletions(-) create mode 100644 apps/web/instrumentation-node.ts create mode 100644 apps/web/lib/otelSetup.ts create mode 100644 apps/web/prometheus.yml diff --git a/.env.example b/.env.example index 6b5b2582f2..ce10efce27 100644 --- a/.env.example +++ b/.env.example @@ -202,4 +202,8 @@ UNKEY_ROOT_KEY= # AI_AZURE_LLM_DEPLOYMENT_ID= # NEXT_PUBLIC_INTERCOM_APP_ID= -# INTERCOM_SECRET_KEY= \ No newline at end of file +# INTERCOM_SECRET_KEY= + +# Enable Prometheus metrics +# PROMETHEUS_ENABLED= +# PROMETHEUS_EXPORTER_PORT= diff --git a/apps/web/instrumentation-node.ts b/apps/web/instrumentation-node.ts new file mode 100644 index 0000000000..a1abee1ca5 --- /dev/null +++ b/apps/web/instrumentation-node.ts @@ -0,0 +1,58 @@ +// instrumentation-node.ts +import { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; +import { HostMetrics } from "@opentelemetry/host-metrics"; +import { registerInstrumentations } from "@opentelemetry/instrumentation"; +import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; +import { RuntimeNodeInstrumentation } from "@opentelemetry/instrumentation-runtime-node"; +import { + Resource, + detectResourcesSync, + envDetector, + hostDetector, + processDetector, +} from "@opentelemetry/resources"; +import { MeterProvider } from "@opentelemetry/sdk-metrics"; +import { env } from "@formbricks/lib/env"; + +const exporter = new PrometheusExporter({ + port: env.PROMETHEUS_EXPORTER_PORT ? parseInt(env.PROMETHEUS_EXPORTER_PORT) : 9464, + endpoint: "/metrics", + host: "0.0.0.0", // Listen on all network interfaces +}); + +const detectedResources = detectResourcesSync({ + detectors: [envDetector, processDetector, hostDetector], +}); + +const customResources = new Resource({}); + +const resources = detectedResources.merge(customResources); + +const meterProvider = new MeterProvider({ + readers: [exporter], + resource: resources, +}); + +const hostMetrics = new HostMetrics({ + name: `otel-metrics`, + meterProvider, +}); + +registerInstrumentations({ + meterProvider, + instrumentations: [new HttpInstrumentation(), new RuntimeNodeInstrumentation()], +}); + +hostMetrics.start(); + +process.on("SIGTERM", async () => { + try { + // Stop collecting metrics or flush them if needed + await meterProvider.shutdown(); + // Possibly close other instrumentation resources + } catch (e) { + console.error("Error during graceful shutdown:", e); + } finally { + process.exit(0); + } +}); diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts index f50fff0225..0b527429c2 100644 --- a/apps/web/instrumentation.ts +++ b/apps/web/instrumentation.ts @@ -1,25 +1,8 @@ -import { registerOTel } from "@vercel/otel"; -import { LangfuseExporter } from "langfuse-vercel"; import { env } from "@formbricks/lib/env"; -export async function register() { - if (env.LANGFUSE_SECRET_KEY && env.LANGFUSE_PUBLIC_KEY && env.LANGFUSE_BASEURL) { - registerOTel({ - serviceName: "formbricks-cloud-dev", - traceExporter: new LangfuseExporter({ - debug: false, - secretKey: env.LANGFUSE_SECRET_KEY, - publicKey: env.LANGFUSE_PUBLIC_KEY, - baseUrl: env.LANGFUSE_BASEURL, - }), - }); +// instrumentation.ts +export const register = async () => { + if (process.env.NEXT_RUNTIME === "nodejs" && env.PROMETHEUS_ENABLED) { + await import("./instrumentation-node"); } - - if (process.env.NEXT_RUNTIME === "nodejs") { - await import("./sentry.server.config"); - } - - if (process.env.NEXT_RUNTIME === "edge") { - await import("./sentry.edge.config"); - } -} +}; diff --git a/apps/web/lib/otelSetup.ts b/apps/web/lib/otelSetup.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 5b382270a6..3593620524 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -27,7 +27,10 @@ const nextConfig = { localeDetection: false, defaultLocale: "en-US", }, - experimental: {}, + experimental: { + instrumentationHook: true, + serverComponentsExternalPackages: ["@opentelemetry/instrumentation"], + }, transpilePackages: ["@formbricks/database", "@formbricks/lib"], images: { remotePatterns: [ @@ -108,6 +111,10 @@ const nextConfig = { }, ], }); + config.resolve.fallback = { + http: false, // Prevents Next.js from trying to bundle 'http' + https: false, + }; return config; }, async headers() { diff --git a/apps/web/package.json b/apps/web/package.json index 3030e71856..2b73f19a24 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,8 +41,13 @@ "@lexical/rich-text": "0.21.0", "@lexical/table": "0.21.0", "@opentelemetry/api-logs": "0.56.0", + "@opentelemetry/exporter-prometheus": "0.57.2", + "@opentelemetry/host-metrics": "0.35.5", "@opentelemetry/instrumentation": "0.56.0", + "@opentelemetry/instrumentation-http": "0.57.2", + "@opentelemetry/instrumentation-runtime-node": "0.12.2", "@opentelemetry/sdk-logs": "0.56.0", + "@opentelemetry/sdk-metrics": "1.30.1", "@paralleldrive/cuid2": "2.2.2", "@prisma/client": "6.0.1", "@radix-ui/react-accordion": "1.2.2", @@ -104,6 +109,7 @@ "next-safe-action": "7.10.2", "node-fetch": "3.3.2", "nodemailer": "6.9.16", + "opentelemetry": "0.1.0", "optional": "0.1.4", "otplib": "12.0.1", "papaparse": "5.4.1", diff --git a/apps/web/prometheus.yml b/apps/web/prometheus.yml new file mode 100644 index 0000000000..f6f61a3fcc --- /dev/null +++ b/apps/web/prometheus.yml @@ -0,0 +1,5 @@ +scrape_configs: + - job_name: "nodejs-app" + scrape_interval: 5s + static_configs: + - targets: ["host.docker.internal:9464"] diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index a63ad6f98f..7281ebf19b 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -60,5 +60,7 @@ These variables are present inside your machine’s docker-compose file. Restart | OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | | | UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | | | CUSTOM_CACHE_DISABLED | Disables custom cache handler if set to 1 (required for deployment on Vercel) | optional | | +| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | | +| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 | | optional | | Note: If you want to configure something that is not possible via above, please open an issue on our GitHub repo here or reach out to us on Github Discussions and we’ll try our best to work out a solution with you. diff --git a/packages/lib/env.ts b/packages/lib/env.ts index 8e3b77900e..95267506dd 100644 --- a/packages/lib/env.ts +++ b/packages/lib/env.ts @@ -102,6 +102,8 @@ export const env = createEnv({ LANGFUSE_BASEURL: z.string().optional(), UNKEY_ROOT_KEY: z.string().optional(), NODE_ENV: z.enum(["development", "production", "test"]).optional(), + PROMETHEUS_EXPORTER_PORT: z.string().optional(), + PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(), }, /* @@ -221,5 +223,7 @@ export const env = createEnv({ UNSPLASH_ACCESS_KEY: process.env.UNSPLASH_ACCESS_KEY, UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY, NODE_ENV: process.env.NODE_ENV, + PROMETHEUS_ENABLED: process.env.PROMETHEUS_ENABLED, + PROMETHEUS_EXPORTER_PORT: process.env.PROMETHEUS_EXPORTER_PORT, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3a0ac2c61..b4f63d48ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,12 +252,27 @@ importers: '@opentelemetry/api-logs': specifier: 0.56.0 version: 0.56.0 + '@opentelemetry/exporter-prometheus': + specifier: 0.57.2 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/host-metrics': + specifier: 0.35.5 + version: 0.35.5(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': specifier: 0.56.0 version: 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': + specifier: 0.57.2 + version: 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-runtime-node': + specifier: 0.12.2 + version: 0.12.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': specifier: 0.56.0 version: 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: 1.30.1 + version: 1.30.1(@opentelemetry/api@1.9.0) '@paralleldrive/cuid2': specifier: 2.2.2 version: 2.2.2 @@ -441,6 +456,9 @@ importers: nodemailer: specifier: 6.9.16 version: 6.9.16 + opentelemetry: + specifier: 0.1.0 + version: 0.1.0 optional: specifier: 0.1.4 version: 0.1.4 @@ -621,7 +639,7 @@ importers: version: 8.18.0(eslint@8.57.0)(typescript@5.7.2) '@vercel/style-guide': specifier: 6.0.0 - version: 6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + version: 6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.7(tsx@4.19.2)) eslint-config-next: specifier: 15.1.0 version: 15.1.0(eslint@8.57.0)(typescript@5.7.2) @@ -3498,6 +3516,10 @@ packages: resolution: {integrity: sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==} engines: {node: '>=14'} + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + '@opentelemetry/api@1.4.1': resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} engines: {node: '>=8.0.0'} @@ -3542,6 +3564,18 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-prometheus@0.57.2': + resolution: {integrity: sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/host-metrics@0.35.5': + resolution: {integrity: sha512-Zf9Cjl7H6JalspnK5KD1+LLKSVecSinouVctNmUxRy+WP+20KwHq+qg4hADllkEmJ99MZByLLmEmzrr7s92V6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-amqplib@0.45.0': resolution: {integrity: sha512-SlKLsOS65NGMIBG1Lh/hLrMDU9WzTUF25apnV6ZmWZB1bBmUwan7qrwwrTu1cL5LzJWCXOdZPuTaxP7pC9qxnQ==} engines: {node: '>=14'} @@ -3602,6 +3636,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-http@0.57.2': + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-ioredis@0.46.0': resolution: {integrity: sha512-sOdsq8oGi29V58p1AkefHvuB3l2ymP1IbxRIX3y4lZesQWKL8fLhBmy8xYjINSQ5gHzWul2yoz7pe7boxhZcqQ==} engines: {node: '>=14'} @@ -3674,6 +3714,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-runtime-node@0.12.2': + resolution: {integrity: sha512-HNBW1rJiHDBTHQlh5oH1IAcV8O5VR7/L5BBOfGAMpGno3Jq9cNqTh96uUp0qBXBuxD8Yl1eoI5N+B5TdmjLteQ==} + engines: {node: '>=17.4.0'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-tedious@0.17.0': resolution: {integrity: sha512-yRBz2409an03uVd1Q2jWMt3SqwZqRFyKoWYYX3hBAtPDazJ4w5L+1VOij71TKwgZxZZNdDBXImTQjii+VeuzLg==} engines: {node: '>=14'} @@ -3698,6 +3744,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.53.0': resolution: {integrity: sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==} engines: {node: '>=14'} @@ -10800,6 +10852,9 @@ packages: openid-client@6.1.7: resolution: {integrity: sha512-JfY/KvQgOutmG2P+oVNKInE7zIh+im1MQOaO7g5CtNnTWMociA563WweiEMKfR9ry9XG3K2HGvj9wEqhCQkPMg==} + opentelemetry@0.1.0: + resolution: {integrity: sha512-8w5sK99P1ZG25WIvHvIa0mSyQ96hl08VQ+1SUrnSg68O85P9ZqjtRwipAftaJW+QvoxxrK7S+Zu9KOqOA+lNhg==} + optional@0.1.4: resolution: {integrity: sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==} @@ -12561,6 +12616,12 @@ packages: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} + systeminformation@5.23.8: + resolution: {integrity: sha512-Osd24mNKe6jr/YoXLLK3k8TMdzaxDffhpCxgkfgBHcapykIkd50HXThM3TCEuHO2pPuCsSx2ms/SunqhU5MmsQ==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + tailwind-merge@2.5.5: resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} @@ -17291,6 +17352,10 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api@1.4.1': {} '@opentelemetry/api@1.9.0': {} @@ -17335,6 +17400,18 @@ snapshots: '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/host-metrics@0.35.5(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + systeminformation: 5.23.8 + '@opentelemetry/instrumentation-amqplib@0.45.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17421,6 +17498,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + '@opentelemetry/instrumentation-ioredis@0.46.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17526,6 +17614,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation-runtime-node@0.12.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + '@opentelemetry/instrumentation-tedious@0.17.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17567,6 +17662,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.12.0 + require-in-the-middle: 7.5.0 + semver: 7.6.3 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/otlp-exporter-base@0.53.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -20685,7 +20792,7 @@ snapshots: next: 15.1.2(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 - '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))': + '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@15.1.0)(eslint@8.57.0)(prettier@3.4.2)(typescript@5.7.2)(vitest@3.0.7(tsx@4.19.2))': dependencies: '@babel/core': 7.26.0 '@babel/eslint-parser': 7.26.5(@babel/core@7.26.0)(eslint@8.57.0) @@ -20693,7 +20800,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2) '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.7.2) eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0) + eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0) @@ -20705,7 +20812,7 @@ snapshots: eslint-plugin-testing-library: 6.5.0(eslint@8.57.0)(typescript@5.7.2) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-unicorn: 51.0.1(eslint@8.57.0) - eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.7(tsx@4.19.2)) prettier-plugin-packagejson: 2.5.8(prettier@3.4.2) optionalDependencies: '@next/eslint-plugin-next': 15.1.0 @@ -20810,7 +20917,7 @@ snapshots: optionalDependencies: vite: 6.0.9(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0) - '@vitest/mocker@3.0.7(vite@6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0))': + '@vitest/mocker@3.0.7(vite@6.2.0(tsx@4.19.2))': dependencies: '@vitest/spy': 3.0.7 estree-walker: 3.0.3 @@ -22909,7 +23016,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0) eslint-plugin-react: 7.37.2(eslint@8.57.0) eslint-plugin-react-hooks: 5.1.0(eslint@8.57.0) @@ -22929,9 +23036,9 @@ snapshots: eslint: 8.57.0 eslint-plugin-turbo: 2.3.3(eslint@8.57.0) - eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0): + eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0)): dependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0) eslint-import-resolver-node@0.3.9: dependencies: @@ -22953,11 +23060,11 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -23012,7 +23119,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -23030,7 +23137,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -23179,7 +23286,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)): + eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0)(typescript@5.7.2)(vitest@3.0.7(tsx@4.19.2)): dependencies: '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -26449,6 +26556,8 @@ snapshots: jose: 5.9.6 oauth4webapi: 3.3.0 + opentelemetry@0.1.0: {} + optional@0.1.4: {} optionator@0.9.4: @@ -28467,6 +28576,8 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.8.1 + systeminformation@5.23.8: {} + tailwind-merge@2.5.5: {} tailwind-merge@3.0.1: {} @@ -29370,7 +29481,7 @@ snapshots: vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.7 - '@vitest/mocker': 3.0.7(vite@6.2.0(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.27.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.7.0)) + '@vitest/mocker': 3.0.7(vite@6.2.0(tsx@4.19.2)) '@vitest/pretty-format': 3.0.7 '@vitest/runner': 3.0.7 '@vitest/snapshot': 3.0.7 diff --git a/turbo.json b/turbo.json index b3dba1dfc9..8235a1c082 100644 --- a/turbo.json +++ b/turbo.json @@ -184,7 +184,9 @@ "VERSION", "WEBAPP_URL", "UNSPLASH_ACCESS_KEY", - "UNKEY_ROOT_KEY" + "UNKEY_ROOT_KEY", + "PROMETHEUS_ENABLED", + "PROMETHEUS_EXPORTER_PORT" ], "outputs": ["dist/**", ".next/**"] }, From aecedfd0828989932757b962ca78657a4f56c337 Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Thu, 13 Mar 2025 02:12:15 -0700 Subject: [PATCH 040/411] fix: Apply security best practices (#4876) Signed-off-by: StepSecurity Bot Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Dhruwang Co-authored-by: Matthias Nannt --- .../workflows/apply-issue-labels-to-pr.yml | 10 ++++++- .github/workflows/build-web.yml | 7 ++++- .github/workflows/chromatic.yml | 13 ++++++--- .github/workflows/cron-surveyStatusUpdate.yml | 5 ++++ .github/workflows/cron-weeklySummary.yml | 7 +++++ .github/workflows/dependency-review.yml | 27 +++++++++++++++++++ .github/workflows/e2e.yml | 15 +++++++---- .github/workflows/labeler.yml | 10 ++++++- .github/workflows/lint.yml | 5 ++++ .github/workflows/pr.yml | 4 +++ .github/workflows/prepare-release.yml | 5 ++++ .github/workflows/release-changesets.yml | 13 ++++++--- .../release-docker-github-experimental.yml | 20 +++++++++----- .github/workflows/release-docker-github.yml | 20 +++++++++----- .github/workflows/release-docker.yml | 16 ++++++++--- .github/workflows/scorecard.yml | 7 ++++- .github/workflows/semantic-pull-requests.yml | 11 +++++--- .github/workflows/sonarqube.yml | 7 ++++- .github/workflows/test.yml | 14 +++++++--- .../workflows/tolgee-missing-key-check.yml | 11 +++++--- .github/workflows/tolgee.yml | 11 +++++--- .../workflows/welcome-new-contributors.yml | 7 ++++- apps/web/Dockerfile | 2 +- packages/react-native/src/types/error.ts | 20 +++++++------- packages/types/errors.ts | 20 +++++++------- 25 files changed, 219 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/apply-issue-labels-to-pr.yml b/.github/workflows/apply-issue-labels-to-pr.yml index 3299b591a8..b15d6e9873 100644 --- a/.github/workflows/apply-issue-labels-to-pr.yml +++ b/.github/workflows/apply-issue-labels-to-pr.yml @@ -5,6 +5,9 @@ on: types: - opened +permissions: + contents: read + jobs: label_on_pr: runs-on: ubuntu-latest @@ -15,8 +18,13 @@ jobs: pull-requests: write steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Apply labels from linked issue to PR - uses: actions/github-script@v5 + uses: actions/github-script@211cb3fefb35a799baa5156f9321bb774fe56294 # v5.2.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 99e9afb4d4..d029e9443c 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -12,7 +12,12 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v3 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: ./.github/actions/dangerous-git-checkout - name: Build & Cache Web Binaries diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 3be713976c..cb0ab7d0c3 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -11,19 +11,24 @@ jobs: name: Run Chromatic runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 20 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Install dependencies run: pnpm install --config.platform=linux --config.architecture=x64 - name: Run Chromatic - uses: chromaui/action@latest + uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # latest with: # ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/cron-surveyStatusUpdate.yml b/.github/workflows/cron-surveyStatusUpdate.yml index 46ab2b4e73..1e92f2f5aa 100644 --- a/.github/workflows/cron-surveyStatusUpdate.yml +++ b/.github/workflows/cron-surveyStatusUpdate.yml @@ -18,6 +18,11 @@ jobs: CRON_SECRET: ${{ secrets.CRON_SECRET }} runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + - name: cURL request if: ${{ env.APP_URL && env.CRON_SECRET }} run: | diff --git a/.github/workflows/cron-weeklySummary.yml b/.github/workflows/cron-weeklySummary.yml index 9516f7870f..dc570a7cf9 100644 --- a/.github/workflows/cron-weeklySummary.yml +++ b/.github/workflows/cron-weeklySummary.yml @@ -7,6 +7,9 @@ on: schedule: # Runs “At 08:00 on Monday.” (see https://crontab.guru) - cron: "0 8 * * 1" +permissions: + contents: read + jobs: cron-weeklySummary: permissions: @@ -16,6 +19,10 @@ jobs: CRON_SECRET: ${{ secrets.CRON_SECRET }} runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 + with: + egress-policy: audit - name: cURL request if: ${{ env.APP_URL && env.CRON_SECRET }} run: | diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..3781c57654 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 53d31bb810..16ab4a2459 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -43,16 +43,21 @@ jobs: --health-timeout=5s --health-retries=5 steps: - - uses: actions/checkout@v3 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: ./.github/actions/dangerous-git-checkout - name: Setup Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 with: node-version: 20.x - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Install dependencies run: pnpm install --config.platform=linux --config.architecture=x64 @@ -112,7 +117,7 @@ jobs: - name: Azure login if: env.AZURE_ENABLED == 'true' - uses: azure/login@v2 + uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -130,7 +135,7 @@ jobs: run: | pnpm test:e2e - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: always() with: name: playwright-report diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index a5445be9ae..18b82b4b33 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -4,6 +4,9 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: labeler: name: Pull Request Labeler @@ -12,7 +15,12 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" # https://github.com/actions/labeler/issues/442#issuecomment-1297359481 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a44cd153f0..47e36da4ec 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,6 +12,11 @@ jobs: timeout-minutes: 15 steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: ./.github/actions/dangerous-git-checkout diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6da3038d27..bc7a6032ad 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -50,6 +50,10 @@ jobs: checks: write statuses: write steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 + with: + egress-policy: audit - name: fail if conditional jobs failed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') run: exit 1 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index cf1997e9e4..c2ad9307f2 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -18,6 +18,11 @@ jobs: runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: ./.github/actions/dangerous-git-checkout diff --git a/.github/workflows/release-changesets.yml b/.github/workflows/release-changesets.yml index 46e1d7a882..ea4037dd3b 100644 --- a/.github/workflows/release-changesets.yml +++ b/.github/workflows/release-changesets.yml @@ -26,23 +26,28 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout Repo - uses: actions/checkout@v2 + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - name: Setup Node.js 18.x - uses: actions/setup-node@v2 + uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2 with: node-version: 18.x - name: Install pnpm - uses: pnpm/action-setup@v2.2.4 + uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4 - name: Install Dependencies run: pnpm install --config.platform=linux --config.architecture=x64 - name: Create Release Pull Request or Publish to npm id: changesets - uses: changesets/action@v1 + uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9 with: # This expects you to have a script called release which does a build for your packages and calls changeset publish publish: pnpm release diff --git a/.github/workflows/release-docker-github-experimental.yml b/.github/workflows/release-docker-github-experimental.yml index ea3dcbdef7..c009debdcd 100644 --- a/.github/workflows/release-docker-github-experimental.yml +++ b/.github/workflows/release-docker-github-experimental.yml @@ -17,6 +17,9 @@ env: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public" +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -28,23 +31,28 @@ jobs: id-token: write steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Set up Depot CLI - uses: depot/setup-action@v1 + uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 # Install the cosign tool except on PR # https://github.com/sigstore/cosign-installer - name: Install cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@v3.5.0 + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@v3 # v3.0.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -54,7 +62,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5 # v5.0.0 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -62,7 +70,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: depot/build-push-action@v1 + uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 with: project: tw0fqmsx3c token: ${{ secrets.DEPOT_PROJECT_TOKEN }} diff --git a/.github/workflows/release-docker-github.yml b/.github/workflows/release-docker-github.yml index ea348eba99..d648ae6760 100644 --- a/.github/workflows/release-docker-github.yml +++ b/.github/workflows/release-docker-github.yml @@ -20,6 +20,9 @@ env: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public" +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -31,23 +34,28 @@ jobs: id-token: write steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: Set up Depot CLI - uses: depot/setup-action@v1 + uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 # Install the cosign tool except on PR # https://github.com/sigstore/cosign-installer - name: Install cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@v3.5.0 + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@v3 # v3.0.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -57,7 +65,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5 # v5.0.0 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -65,7 +73,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: depot/build-push-action@v1 + uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0 with: project: tw0fqmsx3c token: ${{ secrets.DEPOT_PROJECT_TOKEN }} diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index 0c288e6cbe..3f4c3680c7 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -5,6 +5,9 @@ on: tags: - "v*" +permissions: + contents: read + jobs: release-image-on-dockerhub: name: Release on Dockerhub @@ -16,17 +19,22 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public" steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout Repo - uses: actions/checkout@v2 + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - name: Get Release Tag id: extract_release_tag @@ -36,7 +44,7 @@ jobs: echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1 with: context: . file: ./apps/web/Dockerfile diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 483814d9f4..e82bdca819 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,6 +34,11 @@ jobs: # actions: read steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: "Checkout code" uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: @@ -71,6 +76,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 with: sarif_file: results.sarif diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml index 95cbdb7d26..99494775a5 100644 --- a/.github/workflows/semantic-pull-requests.yml +++ b/.github/workflows/semantic-pull-requests.yml @@ -16,7 +16,12 @@ jobs: name: PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v5 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 id: lint_pr_title env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -35,7 +40,7 @@ jobs: revert ossgg - - uses: marocchino/sticky-pull-request-comment@v2 + - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 # When the previous steps fails, the workflow would stop. By adding this # condition you can continue the execution with the populated error message. if: always() && (steps.lint_pr_title.outputs.error_message != null) @@ -54,7 +59,7 @@ jobs: # Delete a previous comment when the issue has been resolved - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 with: header: pr-title-lint-error message: | diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index d5ee858164..65d26fe7b7 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -14,7 +14,12 @@ jobs: name: SonarQube runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93efc057eb..f1bad54204 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,9 @@ name: Tests on: workflow_call: +permissions: + contents: read + jobs: build: name: Unit Tests @@ -10,16 +13,21 @@ jobs: contents: read steps: - - uses: actions/checkout@v3 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: ./.github/actions/dangerous-git-checkout - name: Setup Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 with: node-version: 20.x - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - name: Install dependencies run: pnpm install --config.platform=linux --config.architecture=x64 diff --git a/.github/workflows/tolgee-missing-key-check.yml b/.github/workflows/tolgee-missing-key-check.yml index e4768415f2..1691860ac3 100644 --- a/.github/workflows/tolgee-missing-key-check.yml +++ b/.github/workflows/tolgee-missing-key-check.yml @@ -12,18 +12,23 @@ jobs: check-missing-translations: runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: ${{ github.event.pull_request.base.ref }} - name: Checkout PR - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: ${{ github.event.pull_request.head.sha }} - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 18 diff --git a/.github/workflows/tolgee.yml b/.github/workflows/tolgee.yml index cc7fbcb9da..b6325c3a13 100644 --- a/.github/workflows/tolgee.yml +++ b/.github/workflows/tolgee.yml @@ -15,8 +15,13 @@ jobs: if: github.event.pull_request.merged == true steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # This ensures we get the full git history @@ -36,7 +41,7 @@ jobs: echo "Detected source branch: $SOURCE_BRANCH" - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 18 # Ensure compatibility with your project @@ -75,7 +80,7 @@ jobs: --yes - name: Upload backup as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: tolgee-backup-${{ github.sha }} path: ./tolgee-backup diff --git a/.github/workflows/welcome-new-contributors.yml b/.github/workflows/welcome-new-contributors.yml index eed2897749..332a34e04a 100644 --- a/.github/workflows/welcome-new-contributors.yml +++ b/.github/workflows/welcome-new-contributors.yml @@ -17,7 +17,12 @@ jobs: timeout-minutes: 10 if: github.event.action == 'opened' steps: - - uses: actions/first-interaction@v1 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + + - uses: actions/first-interaction@3c71ce730280171fd1cfb57c00c774f8998586f7 # v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: |- diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 5df93dac6f..2bee6c354f 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine3.20 AS base +FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base # ## step 1: Prune monorepo diff --git a/packages/react-native/src/types/error.ts b/packages/react-native/src/types/error.ts index 4d6898a5ab..3b9639e4fd 100644 --- a/packages/react-native/src/types/error.ts +++ b/packages/react-native/src/types/error.ts @@ -21,16 +21,16 @@ export const err = (error: E): ResultError => ({ export interface ApiErrorResponse { code: - | "not_found" - | "gone" - | "bad_request" - | "internal_server_error" - | "unauthorized" - | "method_not_allowed" - | "not_authenticated" - | "forbidden" - | "network_error" - | "too_many_requests"; + | "not_found" + | "gone" + | "bad_request" + | "internal_server_error" + | "unauthorized" + | "method_not_allowed" + | "not_authenticated" + | "forbidden" + | "network_error" + | "too_many_requests"; message: string; status: number; url?: URL; diff --git a/packages/types/errors.ts b/packages/types/errors.ts index 7a7a3d5bb1..d85e4887d9 100644 --- a/packages/types/errors.ts +++ b/packages/types/errors.ts @@ -121,16 +121,16 @@ export type { NetworkError, ForbiddenError }; export interface ApiErrorResponse { code: - | "not_found" - | "gone" - | "bad_request" - | "internal_server_error" - | "unauthorized" - | "method_not_allowed" - | "not_authenticated" - | "forbidden" - | "network_error" - | "too_many_requests"; + | "not_found" + | "gone" + | "bad_request" + | "internal_server_error" + | "unauthorized" + | "method_not_allowed" + | "not_authenticated" + | "forbidden" + | "network_error" + | "too_many_requests"; message: string; status: number; url?: URL; From f227c9e97eb24a024d994ce7ee2a60ca02bbdddb Mon Sep 17 00:00:00 2001 From: Piyush Jain <122745947+d3vb0ox@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:00:17 +0530 Subject: [PATCH 041/411] feat: introduce updated helm chart (#4896) Co-authored-by: Matthias Nannt --- .gitignore | 17 + docs/self-hosting/setup/kubernetes.mdx | 271 +++--- helm-chart/Chart.lock | 15 +- helm-chart/Chart.yaml | 33 +- helm-chart/README.md | 862 ++++-------------- helm-chart/charts/postgresql-15.5.36.tgz | Bin 75785 -> 0 bytes helm-chart/charts/postgresql-16.4.16.tgz | Bin 0 -> 81305 bytes helm-chart/charts/redis-20.1.5.tgz | Bin 103244 -> 0 bytes helm-chart/charts/redis-20.11.2.tgz | Bin 0 -> 108485 bytes helm-chart/charts/traefik-32.0.0.tgz | Bin 246624 -> 0 bytes helm-chart/templates/NOTES.txt | 208 +++-- helm-chart/templates/_helpers.tpl | 140 ++- helm-chart/templates/cronjob.yaml | 102 +++ helm-chart/templates/deployment.yaml | 180 +++- helm-chart/templates/externalsecrets.yaml | 52 ++ helm-chart/templates/hpa.yaml | 48 +- helm-chart/templates/ingress.yaml | 58 +- helm-chart/templates/secrets.yaml | 46 +- helm-chart/templates/service.yaml | 54 +- helm-chart/templates/serviceaccount.yaml | 22 + .../templates/tests/test-connection.yaml | 15 - helm-chart/templates/traefik-configmap.yaml | 8 - helm-chart/values.yaml | 373 +++++--- infra/terraform/.terraform.lock.hcl | 164 ++++ infra/terraform/bootstrap.tf | 177 ++++ infra/terraform/data.tf | 12 + infra/terraform/iam.tf | 30 + infra/terraform/main.tf | 668 ++++++++++++++ infra/terraform/provider.tf | 31 + infra/terraform/secrets.tf | 33 + infra/terraform/variables.tf | 1 + infra/terraform/versions.tf | 18 + 32 files changed, 2492 insertions(+), 1146 deletions(-) delete mode 100644 helm-chart/charts/postgresql-15.5.36.tgz create mode 100644 helm-chart/charts/postgresql-16.4.16.tgz delete mode 100644 helm-chart/charts/redis-20.1.5.tgz create mode 100644 helm-chart/charts/redis-20.11.2.tgz delete mode 100644 helm-chart/charts/traefik-32.0.0.tgz create mode 100644 helm-chart/templates/cronjob.yaml create mode 100644 helm-chart/templates/externalsecrets.yaml create mode 100644 helm-chart/templates/serviceaccount.yaml delete mode 100644 helm-chart/templates/tests/test-connection.yaml delete mode 100644 helm-chart/templates/traefik-configmap.yaml create mode 100644 infra/terraform/.terraform.lock.hcl create mode 100644 infra/terraform/bootstrap.tf create mode 100644 infra/terraform/data.tf create mode 100644 infra/terraform/iam.tf create mode 100644 infra/terraform/main.tf create mode 100644 infra/terraform/provider.tf create mode 100644 infra/terraform/secrets.tf create mode 100644 infra/terraform/variables.tf create mode 100644 infra/terraform/versions.tf diff --git a/.gitignore b/.gitignore index ff5a31c9e6..aa874edc93 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,20 @@ apps/web/public/js packages/database/migrations branch.json .vercel + +# Terraform +infra/terraform/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfstate +**/terraform.tfstate.* +**/crash.log +**/override.tf +**/override.tf.json +**/*.tfvars +**/*.tfvars.json +**/.terraformrc +**/terraform.rc + +# IntelliJ IDEA +/.idea/ +/*.iml diff --git a/docs/self-hosting/setup/kubernetes.mdx b/docs/self-hosting/setup/kubernetes.mdx index 8952aa40b0..0f4231708f 100644 --- a/docs/self-hosting/setup/kubernetes.mdx +++ b/docs/self-hosting/setup/kubernetes.mdx @@ -1,154 +1,217 @@ --- title: "Kubernetes Deployment" -description: "Deploy Formbricks on a Kubernetes cluster using Helm." +description: "Deploy the new Helm chart on a Kubernetes cluster using Helm." icon: "circle-nodes" --- -This guide explains how to deploy Formbricks on a Kubernetes cluster using Helm. The primary focus is on deploying Formbricks pods in a production-ready environment with external database services. +# **🚀 Kubernetes Deployment Guide** -## Prerequisites +This guide explains how to deploy the **Formbricks Helm Chart** on a Kubernetes cluster using Helm. It provides configuration options for **internal** and **external** databases, caching services, and secrets management. -Before you begin, ensure that: +--- -- You have a running Kubernetes cluster (AWS EKS, GCP GKE, Azure AKS, Minikube, etc.) -- An Ingress controller (e.g., Traefik, Nginx) is configured -- You have Helm installed on your local machine -- For production environments, you have access to external PostgreSQL and Redis services +## **📌 Prerequisites** +Ensure you have the following before proceeding: -> **Important:** Running multiple Formbricks pods in a cluster setup requires a Formbricks Enterprise license. With the Community Edition, only a single Formbricks pod is supported. Redis is required when deploying multiple Formbricks pods for proper session handling and caching. +- A running Kubernetes cluster (EKS, GKE, AKS, Minikube, etc.) +- An **Ingress Controller** (e.g., Traefik, Nginx) +- **Helm installed** on your local machine +- **Production setup requires managed PostgreSQL and Redis services** -## Basic Installation +> **Note:** Redis is required for **session handling** when deploying multiple pods. -### Step 1: Clone the Formbricks Helm Chart +--- +## **1️⃣ Installation Steps** + +### **🔹 Step 1: Clone the Helm Chart** ```sh -git clone https://github.com/formbricks/formbricks.git -cd formbricks/helm-chart +git clone https://github.com/formbricks/formbricks +cd helm-chart ``` -### Step 2: Deploy Formbricks - -For a basic deployment with a single pod (Community Edition) and PostgreSQL running in the cluster: - +### **🔹 Step 2: Install with Default Configuration** ```sh -helm install my-formbricks ./ \ - --namespace formbricks \ - --set redis.enabled=false \ - --create-namespace +helm install formbricks ./ -n formbricks --create-namespace ``` +By default: +- PostgreSQL and Redis **are deployed within the cluster**. +- Secrets **are dynamically generated** and stored as Kubernetes Secrets. -## Production Deployment - -For production environments, we recommend using managed database and cache services like AWS RDS for PostgreSQL and AWS ElastiCache for Redis: - +### **🔹 Step 3: Install with an Enterprise License** ```sh -helm install my-formbricks ./ \ - --namespace formbricks \ - --create-namespace \ - --set replicaCount=3 \ - --set postgresql.enabled=false \ - --set postgresql.externalUrl="postgresql://user:password@your-postgres-host:5432/formbricks" \ - --set redis.enabled=false \ - --set redis.externalUrl="redis://your-redis-host:6379" +helm install formbricks ./ -n formbricks --create-namespace --set enterprise.licenseKey="YOUR_LICENSE_KEY" ``` -> **Note:** The above multi-pod configuration requires a Formbricks Enterprise license. Redis is enabled and configured to support multiple Formbricks pods. +--- -## Verify Installation +## **2️⃣ Configuring Secrets** -### Check Running Services +### **🔹 Using Kubernetes Secrets (Default)** +By default, **secrets are stored as Kubernetes Secrets**. +The chart automatically generates **random values** for required secrets. +Modify `values.yaml`: +```yaml +secret: + enabled: true +``` + +--- + +### **🔹 Using External Secrets (AWS Secrets Manager, Vault, etc.)** +To use an **external secrets manager**, enable `externalSecret` in `values.yaml`: +```yaml +secret: + enabled: false # Disable default secret generation + +externalSecret: + enabled: true + secretStore: + name: aws-secrets-manager + kind: ClusterSecretStore + refreshInterval: "1h" + files: + redis: + data: + REDIS_PASSWORD: + remoteRef: + key: "prod/formbricks/secrets" + property: REDIS_PASSWORD + postgres: + data: + POSTGRES_ADMIN_PASSWORD: + remoteRef: + key: "prod/formbricks/secrets" + property: POSTGRES_ADMIN_PASSWORD + POSTGRES_USER_PASSWORD: + remoteRef: + key: "prod/formbricks/secrets" + property: POSTGRES_USER_PASSWORD + app-secrets: + data: + DATABASE_URL: + remoteRef: + key: "prod/formbricks/secrets" + property: DATABASE_URL + REDIS_URL: + remoteRef: + key: "prod/formbricks/secrets" + property: REDIS_URL + ENCRYPTION_KEY: + remoteRef: + key: "prod/formbricks/secrets" + property: ENCRYPTION_KEY +``` +📌 **Ensure ExternalSecrets Operator is installed:** +[https://external-secrets.io/latest/](https://external-secrets.io/latest/) + +Install with: ```sh -kubectl get pods -n formbricks -kubectl get svc -n formbricks -kubectl get ingress -n formbricks +helm install formbricks ./ -n formbricks --create-namespace -f values.yaml ``` -> **Note:** The Formbricks application pod may take some time to reach a stable state as it runs database migrations during startup. +--- -### Access Formbricks +## **3️⃣ Configuring PostgreSQL and Redis** -- If running locally with Minikube: - ```sh - minikube service my-formbricks -n formbricks - ``` -- If deployed on a cloud cluster, make sure to set up your ingress controller properly and visit the domain or IP address associated with your ingress. +### **🔹 Using Managed PostgreSQL and Redis** +For production, we recommend using **managed database and cache services**. -## Upgrading Formbricks +Modify `values.yaml`: +```yaml +postgresql: + enabled: false + externalDatabaseUrl: "postgresql://user:password@your-postgres-host:5432/mydb" -To upgrade your Formbricks deployment, use: - -```bash -# From the helm-chart directory -helm upgrade my-formbricks ./ --namespace formbricks +redis: + enabled: false + externalRedisUrl: "redis://your-redis-host:6379" +``` +Install with: +```sh +helm install formbricks ./ -n formbricks --create-namespace -f values.yaml ``` -### Common Upgrade Scenarios +--- -#### 1. Updating Environment Variables +### **🔹 Using In-Cluster PostgreSQL and Redis (Default)** +By default, PostgreSQL and Redis are **deployed inside the cluster**. +To **ensure in-cluster deployment**, use: -```bash -helm upgrade my-formbricks ./ --namespace formbricks \ - --set env.SMTP_HOST=new-smtp.example.com \ - --set env.SMTP_PORT=587 \ - --set env.NEW_CUSTOM_VAR=newvalue +```yaml +postgresql: + enabled: true + +redis: + enabled: true +``` +Apply with: +```sh +helm install formbricks ./ -n formbricks --create-namespace -f values.yaml ``` -#### 2. Scaling Resources +--- -```bash -helm upgrade my-formbricks ./ --namespace formbricks \ - --set resources.limits.cpu=1 \ - --set resources.limits.memory=2Gi \ - --set resources.requests.cpu=500m \ - --set resources.requests.memory=1Gi +## **4️⃣ Upgrading the Deployment** +To apply changes: +```sh +helm upgrade formbricks ./ -n formbricks ``` -#### 3. Updating Autoscaling Configuration - -```bash -helm upgrade my-formbricks ./ --namespace formbricks \ - --set autoscaling.enabled=true \ - --set autoscaling.minReplicas=3 \ - --set autoscaling.maxReplicas=10 \ - --set autoscaling.metrics[0].resource.target.averageUtilization=75 +### **🔹 Scaling Resources** +```sh +helm upgrade formbricks ./ -n formbricks --set deployment.resources.limits.cpu=2 --set deployment.resources.limits.memory=4Gi ``` -> **Note:** Enabling autoscaling requires a Formbricks Enterprise license and proper Redis configuration. - -#### 4. Changing Database Connection - -```bash -helm upgrade my-formbricks ./ --namespace formbricks \ - --set postgresql.enabled=false \ - --set postgresql.externalUrl="postgresql://newuser:newpassword@external-postgres-host:5432/newdatabase" +### **🔹 Enabling Autoscaling** +```sh +helm upgrade formbricks ./ -n formbricks --set autoscaling.enabled=true --set autoscaling.minReplicas=3 --set autoscaling.maxReplicas=10 ``` -## Advanced Configuration Options +--- -For advanced configurations including: +## **5️⃣ Key Configuration Values** -- Deploying PostgreSQL and Redis within your Kubernetes cluster -- Configuring Traefik ingress controller -- Setting up high availability -- Customizing autoscaling behavior +| Field | Description | Default Value | +|--------------------------------|--------------------------------------|--------------| +| `deployment.replicas` | Number of application replicas | `1` | +| `deployment.image.repository` | Docker image repository | `"ghcr.io/formbricks/formbricks"` | +| `deployment.image.tag` | Docker image tag | `"latest"` | +| `autoscaling.enabled` | Enable autoscaling | `false` | +| `postgresql.enabled` | Deploy PostgreSQL in cluster | `true` | +| `postgresql.externalDatabaseUrl` | External PostgreSQL URL | `""` | +| `redis.enabled` | Deploy Redis in cluster | `true` | +| `redis.externalRedisUrl` | External Redis URL | `""` | +| `externalSecret.enabled` | Enable external secrets manager | `false` | -Please refer to the complete Helm chart documentation at: -[https://github.com/formbricks/formbricks/tree/main/helm-chart](https://github.com/formbricks/formbricks/tree/main/helm-chart) +📌 **Refer to the Helm chart repository for full configuration options.** -## Key Configuration Values +--- -| Field | Description | Default | -| ------------------------- | ----------------------------- | ------------------------------- | -| `replicaCount` | Number of Formbricks replicas | `1` | -| `image.repository` | Docker image repository | `ghcr.io/formbricks/formbricks` | -| `image.tag` | Docker image tag | `"2.6.0"` | -| `resources.limits.cpu` | CPU resource limit | `500m` | -| `resources.limits.memory` | Memory resource limit | `1Gi` | -| `autoscaling.enabled` | Enable autoscaling | `false` | -| `postgresql.enabled` | Deploy PostgreSQL in cluster | `true` | -| `postgresql.externalUrl` | External PostgreSQL URL | `""` | -| `redis.enabled` | Deploy Redis in cluster | `false` | -| `redis.externalUrl` | External Redis URL | `""` | +## **6️⃣ Uninstalling the Deployment** +To remove the deployment: +```sh +helm uninstall formbricks -n formbricks +``` -For the complete list of configuration options, please refer to the Formbricks Helm chart repository. +### **Removing Persistent Volumes (PVCs)** +By default, **PVCs are not deleted** with Helm. +To manually remove them: +```sh +kubectl delete pvc --all -n formbricks +``` + +To completely delete the namespace: +```sh +kubectl delete namespace formbricks +``` + +--- + +## **📢 Additional Notes** +- **Ingress Setup:** If using an ingress controller, make sure to configure `ingress.enabled: true` in `values.yaml`. +- **Environment Variables:** Pass custom environment variables via `envFrom` in `values.yaml`. +- **Backup Strategy:** Ensure you have a backup policy for PostgreSQL if running in-cluster. + +🚀 **Your Formbricks deployment is now ready!** 🚀 diff --git a/helm-chart/Chart.lock b/helm-chart/Chart.lock index daec62b07c..fe0ada4065 100644 --- a/helm-chart/Chart.lock +++ b/helm-chart/Chart.lock @@ -1,12 +1,9 @@ dependencies: - name: postgresql - repository: https://charts.bitnami.com/bitnami - version: 15.5.36 + repository: oci://registry-1.docker.io/bitnamicharts + version: 16.4.16 - name: redis - repository: https://charts.bitnami.com/bitnami - version: 20.1.5 -- name: traefik - repository: https://helm.traefik.io/traefik - version: 32.0.0 -digest: sha256:21923a92e214351c3f96348fe0c479cc6e98e7828d75d41edc1ab73839dd39ce -generated: "2024-09-27T13:48:24.815107+03:00" + repository: oci://registry-1.docker.io/bitnamicharts + version: 20.11.2 +digest: sha256:6233567e6d133fd87585de7cb11f835125ab649fc7979eac7b17d4b2881f54dc +generated: "2025-03-06T15:48:20.190945+05:30" diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index 959ebb3f8d..7fb8b04f60 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -1,19 +1,30 @@ apiVersion: v2 name: formbricks -description: A Helm chart for Formbricks with PostgreSQL, Redis, Traefik, and cert-manager +description: A Helm chart for Formbricks with PostgreSQL, Redis + type: application -version: 0.1.2 -appVersion: "1.0.0" + +# Helm chart Version +version: 3.3.1 +appVersion: v3.3.1 + +keywords: + - formbricks + - postgresql + - redis + +home: https://formbricks.com/docs/self-hosting/setup/kubernetes +maintainers: + - name: Formbricks + email: info@formbricks.com + + dependencies: - name: postgresql - version: 15.5.36 - repository: https://charts.bitnami.com/bitnami + version: "16.4.16" + repository: "oci://registry-1.docker.io/bitnamicharts" condition: postgresql.enabled - name: redis - version: 20.1.5 - repository: https://charts.bitnami.com/bitnami + version: 20.11.2 + repository: "oci://registry-1.docker.io/bitnamicharts" condition: redis.enabled - - name: traefik - version: 32.0.0 - repository: https://helm.traefik.io/traefik - condition: traefik.enabled diff --git a/helm-chart/README.md b/helm-chart/README.md index ddb58973ce..8138cf9206 100644 --- a/helm-chart/README.md +++ b/helm-chart/README.md @@ -1,706 +1,156 @@ -
- -

- - - -Open Source Privacy First Experience Management Solution Qualtrics Alternative Logo - - - -

Formbricks

- -

-Harvest user-insights, build irresistible experiences. -
-
Website -

-

- -# Formbricks Helm Chart: Comprehensive Documentation - -- [Formbricks Helm Chart: Comprehensive Documentation](#formbricks-helm-chart-comprehensive-documentation) - - [Introduction](#introduction) - - [Prerequisites](#prerequisites) - - [Chart Components](#chart-components) - - [Installation](#installation) - - [Quick Start](#quick-start) - - [Usage Examples](#usage-examples) - - [Scaling PostgreSQL and Redis](#scaling-postgresql-and-redis) - - [Configuration](#configuration) - - [Environment Variables](#environment-variables) - - [Scaling](#scaling) - - [With Auto Scaling (Kubernetes Metrics Server Requirement)](#with-auto-scaling-kubernetes-metrics-server-requirement) - - [Customizing Autoscaling](#customizing-autoscaling) - - [Kubernetes Metrics Server Requirement](#kubernetes-metrics-server-requirement) - - [Advanced Autoscaling Configuration](#advanced-autoscaling-configuration) - - [Upgrading Formbricks](#upgrading-formbricks) - - [Upgrade Process](#upgrade-process) - - [Common Upgrade Scenarios](#common-upgrade-scenarios) - - [1. Updating Environment Variables](#1-updating-environment-variables) - - [2. Enabling or Disabling Features](#2-enabling-or-disabling-features) - - [3. Scaling Resources](#3-scaling-resources) - - [4. Updating Autoscaling Configuration](#4-updating-autoscaling-configuration) - - [5. Changing Database Credentials](#5-changing-database-credentials) - - [Using a Values File for Complex Upgrades](#using-a-values-file-for-complex-upgrades) - - [Support](#support) - - [Full Values Documentation](#full-values-documentation) - - [✍️ Contribution](#️-contribution) - - [MicroK8s Installation and Formbricks Deployment](#microk8s-installation-and-formbricks-deployment) - - [MicroK8s Quick Setup](#microk8s-quick-setup) - - [Deploying Formbricks on MicroK8s](#deploying-formbricks-on-microk8s) - -## Introduction - -This Helm chart deploys Formbricks, an advanced open-source form builder and survey tool, along with its required dependencies (PostgreSQL and Redis) on a Kubernetes cluster. It also includes an optional Traefik ingress controller for easy access and SSL termination. - -## Prerequisites - -Before installing the Formbricks Helm chart, ensure you have the following: - -- Kubernetes cluster version 1.24 or later -- Helm version 3.2.0 or later -- Dynamic volume provisioning support in the underlying infrastructure (for PostgreSQL persistence) -- Familiarity with Kubernetes concepts and Helm charts - -## Chart Components - -This Helm chart deploys the following components: - -1. **Formbricks Application**: The core Formbricks service. -2. **PostgreSQL Database**: (Optional) A relational database for Formbricks data. -3. **Redis**: (Optional) An in-memory data structure store for caching. -4. **Traefik Ingress Controller**: (Optional) A modern HTTP reverse proxy and load balancer. - -## Installation - -### Quick Start - -To quickly deploy Formbricks with default settings: - -1. clone the formbricks repository and navigate to the helm-chart directory: - - ```bash - git clone https://github.com/formbricks/formbricks.git - cd formbricks/helm-chart - ``` - -2. Deploy Formbricks - - ```bash - helm install my-formbricks ./ \ - --namespace formbricks \ - --create-namespace \ - --set replicaCount=2 - ``` - -This will deploy Formbricks with default settings, including a new PostgreSQL instance, Redis and Traefik disabled. - -### Verify and Access Formbricks - -After deploying Formbricks, you can verify the installation and access the application: - -1. Check the Running Services: - - ```bash - kubectl get pods -n formbricks - kubectl get svc -n formbricks - kubectl get ingress -n formbricks - ``` - - > **Note:** The Formbricks application pod may take some time to reach a stable state as it runs database migrations during startup. - -2. Access Formbricks: - - If running locally with **Minikube**: - ```bash - minikube service my-formbricks -n formbricks - ``` - - If deployed on a **cloud cluster**, visit: - ``` - https://formbricks.example.com - ``` - (Replace with your configured hostname) - -### Usage Examples - -Here are various examples of how to install and configure the Formbricks Helm chart: - -1. **Default Installation with Traefik enabled and Custom Hostname**: - -
- - Option 1: Installation without SSL (Not recommended for production) - - ```bash - helm install my-formbricks formbricks/formbricks \ - --namespace formbricks \ - --create-namespace \ - --set traefik.enabled=true \ - --set hostname=forms.example.com - ``` - -```` - -This command enables Traefik and sets a custom hostname. Replace `forms.example.com` with your actual domain name. - -
- -
- Option 2: Installation with SSL (Recommended for production) - -1. First, download the values file: - -```bash -helm show values formbricks/formbricks > values.yaml -``` - -2. Open the `values.yaml` file in a text editor and make the following changes: - -```yaml -traefik: - enabled: true - additionalArguments: - - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com" -``` - -Replace `your-email@example.com` with a valid email address where you want to receive Let's Encrypt notifications. - -3. Install Formbricks with the updated values file: - -```bash -helm install my-formbricks formbricks/formbricks \ - -f values.yaml \ - --namespace formbricks \ - --create-namespace \ - --set hostname=forms.example.com -``` - -This command enables Traefik, sets a custom hostname, and uses the configured email address for Let's Encrypt. Remember to replace `forms.example.com` with your actual domain name. - -
- -These installation options provide flexibility in setting up Formbricks with Traefik. The SSL option is recommended for production environments to ensure secure communications. - -2. **Community Advanced:** - Provision a whole community setup with Formbricks, Postgres, Custom Domain with SSL - - ```bash - helm install formbricks formbricks/formbricks \ - --namespace formbricks \ - --create-namespace \ - --set postgres.enabled=true \ - --set traefik.enabled=true \ - --set hostname=forms.example.com \ - --set email=your-mail@example.com - ``` - -3. **Cluster Advanced:** - Provision a ready to use cluster for enterprise customers with Formbricks (3 pods), Postgres, Redis and Custom Domain with SSL - - ```bash - helm install formbricks formbricks/formbricks \ - --namespace formbricks \ - --create-namespace \ - --set replicaCount=3 - --set redis.enabled=true \ - --set traefik.enabled=true \ - --set hostname=forms.example.com \ - --set email=your-mail@example.com - ``` - -4. **Installation with Redis and PostgreSQL Disabled, Using External Services**: - - ```bash - helm install my-formbricks formbricks/formbricks \ - --namespace formbricks \ - --create-namespace \ - --set postgresql.enabled=false \ - --set postgresql.externalUrl=postgresql://user:password@your-postgres-url:5432/dbname \ - --set redis.enabled=false \ - --set redis.externalUrl=redis://your-redis-url:6379 - ``` - -5. **High Availability Setup**: - ```bash - helm install my-formbricks formbricks/formbricks \ - --namespace formbricks \ - --create-namespace \ - --set replicaCount=3 - ``` - -This command: - -1. Deploys the Formbricks application with 3 replicas. -2. Enables PostgreSQL and Redis with default settings. - -#### Scaling PostgreSQL and Redis - -For advanced configuration and scaling of PostgreSQL and Redis, refer to their respective Helm chart documentation: - -- PostgreSQL Helm Chart: https://github.com/bitnami/charts/tree/master/bitnami/postgresql -- Redis Helm Chart: https://github.com/bitnami/charts/tree/master/bitnami/redis - -These documents provide detailed information on scaling and configuring high availability for each component. - -4. **Custom Configuration with Environment Variables**: - - ```bash - helm install my-formbricks formbricks/formbricks \ - --namespace formbricks \ - --create-namespace \ - --set env.SMTP_HOST=smtp.example.com \ - --set env.SMTP_PORT=587 \ - --set env.SMTP_USER=user@example.com \ - --set env.SMTP_PASSWORD=password123 \ - --set env.SMTP_AUTHENTICATED=1 - ``` - -5. **Installation with Custom Resource Limits**: - ```bash - helm install my-formbricks formbricks/formbricks \ - --namespace formbricks \ - --create-namespace \ - --set resources.limits.cpu=1 \ - --set resources.limits.memory=1Gi \ - --set resources.requests.cpu=500m \ - --set resources.requests.memory=512Mi - ``` - -### Configuration - -For detailed configuration options, please refer to the [Full Values Documentation](#full-values-documentation) section at the end of this document. - -## Environment Variables - -Formbricks supports various environment variables for configuration. Here are some key variables: - -| Variable | Description | Required | Default | -| ----------------- | -------------------------------- | -------- | ----------------------- | -| `WEBAPP_URL` | Base URL of the site | Yes | `http://localhost:3000` | -| `NEXTAUTH_URL` | Location of the auth server | Yes | `http://localhost:3000` | -| `DATABASE_URL` | Database URL with credentials | Yes | - | -| `NEXTAUTH_SECRET` | Secret for NextAuth | Yes | (Generated) | -| `ENCRYPTION_KEY` | Secret for data encryption | Yes | (Generated) | -| `CRON_SECRET` | API Secret for running cron jobs | Yes | (Generated) | -| `...` | ... | ... | ... | - -For a comprehensive list of supported environment variables, refer to the [Formbricks Configuration Documentation](https://formbricks.com/docs/self-hosting/configuration). - -## Scaling - -```bash -kubectl scale deployment my-formbricks --replicas=5 -n formbricks -``` - -This command scales the Formbricks deployment to 5 replicas. Replace `my-formbricks` with your actual deployment name if different. - -### With Auto Scaling (Kubernetes Metrics Server Requirement) - -The Formbricks Helm chart includes support for Horizontal Pod Autoscaling (HPA) to automatically adjust the number of pods based on CPU utilization. This feature is enabled by default and can be customized to suit your specific needs. - -```bash -helm install my-formbricks formbricks/formbricks --namespace formbricks --create-namespace \ - --set autoscaling.enabled=true -``` - -This configuration sets up autoscaling with a minimum of 2 replicas and a maximum of 5 replicas, targeting an average CPU utilization of 80% - -### Customizing Autoscaling - -To adjust the autoscaling settings, you can modify the values in your `values.yaml` file or use the `--set` flag when installing or upgrading the chart. Here are some common customizations: - -1. Change the minimum and maximum number of replicas: - -```bash -helm install my-formbricks formbricks/formbricks \ - --set autoscaling.enabled=true \ - --set autoscaling.minReplicas=3 \ - --set autoscaling.maxReplicas=10 -``` - -2. Adjust the target CPU utilization: - -```bash -helm install my-formbricks formbricks/formbricks \ - --set autoscaling.enabled=true \ - --set autoscaling.metrics[0].resource.target.averageUtilization=70 -``` - -3. Disable autoscaling: - -```bash -helm upgrade my-formbricks formbricks/formbricks \ - --set autoscaling.enabled=false -``` - -### Kubernetes Metrics Server Requirement - -For autoscaling to function properly, the Kubernetes Metrics Server must be installed in your cluster. The Metrics Server collects resource metrics from Kubelets and exposes them in the Kubernetes API server through the Metrics API. - -If you don't have the Metrics Server installed, you can typically add it using the following command: - -```bash -kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml -``` - -For more detailed information on installing and configuring the Metrics Server, please refer to the [official Kubernetes Metrics Server documentation](https://github.com/kubernetes-sigs/metrics-server). - -### Advanced Autoscaling Configuration - -The Formbricks Helm chart uses Kubernetes HPA v2, which allows for more advanced scaling behaviors. You can customize the `behavior` section in the `values.yaml` file to fine-tune how your application scales up and down. For more information on advanced HPA configurations, refer to the [Kubernetes HPA documentation](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/). - -## Upgrading Formbricks - -This section provides guidance on how to upgrade your Formbricks deployment using Helm, including examples of common upgrade scenarios. - -### Upgrade Process - -To upgrade your Formbricks deployment, use the `helm upgrade` command. Always ensure you have the latest version of the Formbricks Helm chart by running `helm repo update` before upgrading. - -```bash -helm repo update -helm upgrade my-formbricks formbricks/formbricks --namespace formbricks -``` - -### Common Upgrade Scenarios - -#### 1. Updating Environment Variables - -To update or add new environment variables, use the `--set` flag with the `env` prefix: - -```bash -helm upgrade my-formbricks formbricks/formbricks \ - --set env.SMTP_HOST=new-smtp.example.com \ - --set env.SMTP_PORT=587 \ - --set env.NEW_CUSTOM_VAR=newvalue -``` - -This command updates the SMTP host and port, and adds a new custom environment variable. - -#### 2. Enabling or Disabling Features - -You can enable or disable features by updating their respective values: - -```bash -# Disable Redis -helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=false - -# Enable Redis -helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=true -``` - -#### 3. Scaling Resources - -To adjust resource allocation: - -```bash -helm upgrade my-formbricks formbricks/formbricks \ - --set resources.limits.cpu=1 \ - --set resources.limits.memory=2Gi \ - --set resources.requests.cpu=500m \ - --set resources.requests.memory=1Gi -``` - -#### 4. Updating Autoscaling Configuration - -To modify autoscaling settings: - -```bash -helm upgrade my-formbricks formbricks/formbricks \ - --set autoscaling.minReplicas=3 \ - --set autoscaling.maxReplicas=10 \ - --set autoscaling.metrics[0].resource.target.averageUtilization=75 -``` - -#### 5. Changing Database Credentials - -To update PostgreSQL database credentials: -To switch from the built-in PostgreSQL to an external database or update the external database credentials: - -```bash -helm upgrade my-formbricks formbricks/formbricks \ - --set postgresql.enabled=false \ - --set postgresql.externalUrl="postgresql://newuser:newpassword@external-postgres-host:5432/newdatabase" -``` - -This command disables the built-in PostgreSQL and configures Formbricks to use an external PostgreSQL database. Make sure your external database is set up and accessible before making this change. - -### Using a Values File for Complex Upgrades - -For more complex upgrades or when you need to change multiple values, it's recommended to use a values file: - -1. Create a file named `upgrade-values.yaml` with your desired changes: - - ```yaml - env: - SMTP_HOST: new-smtp.example.com - SMTP_PORT: "587" - resources: - limits: - cpu: 1 - memory: 2Gi - autoscaling: - minReplicas: 3 - maxReplicas: 10 - traefik: - enabled: true - postgresql: - auth: - username: newuser - password: newpassword - database: newdatabase - ``` - -2. Apply the upgrade using the values file: - - ```bash - helm upgrade my-formbricks formbricks/formbricks -f upgrade-values.yaml - ``` - -Remember to always backup your data before performing upgrades, especially when modifying database-related settings. - -## Support - -For support with the Formbricks Helm chart: - -- Open an issue on the [Formbricks GitHub repository](https://github.com/formbricks/formbricks) -- Get help on [Github Discussions](https://github.com/formbricks/formbricks/discussions) -- For enterprise support, contact us at hola@formbricks.com - -## Full Values Documentation - -Below is a comprehensive list of all configurable values in the Formbricks Helm chart: - -| Field | Description | Default | -| ----------------------------------------------------------- | ------------------------------------------ | ------------------------------- | -| `image.repository` | Docker image repository for Formbricks | `ghcr.io/formbricks/formbricks` | -| `image.pullPolicy` | Image pull policy | `IfNotPresent` | -| `image.tag` | Docker image tag | `"2.6.0"` | -| `service.type` | Kubernetes service type | `ClusterIP` | -| `service.port` | Kubernetes service port | `80` | -| `service.targetPort` | Container port to expose | `3000` | -| `resources.limits.cpu` | CPU resource limit | `500m` | -| `resources.limits.memory` | Memory resource limit | `1Gi` | -| `resources.requests.cpu` | Memory resource request | `null` | -| `resources.requests.memory` | Memory resource request | `null` | -| `autoscaling.enabled` | Enable autoscaling | `false` | -| `autoscaling.minReplicas` | Minimum number of replicas | `1` | -| `autoscaling.maxReplicas` | Maximum number of replicas | `5` | -| `autoscaling.metrics[0].type` | Type of metric for autoscaling | `Resource` | -| `autoscaling.metrics[0].resource.name` | Resource name for autoscaling metric | `cpu` | -| `autoscaling.metrics[0].resource.target.type` | Target type for autoscaling | `Utilization` | -| `autoscaling.metrics[0].resource.target.averageUtilization` | Average utilization target for autoscaling | `80` | -| `autoscaling.behavior.scaleDown.stabilizationWindowSeconds` | Stabilization window for scaling down | `300` | -| `autoscaling.behavior.scaleUp.stabilizationWindowSeconds` | Stabilization window for scaling up | `0` | -| `replicaCount` | Number of replicas | `1` | -| `formbricksConfig.nextAuthSecret` | NextAuth secret | `""` | -| `formbricksConfig.encryptionKey` | Encryption key | `""` | -| `formbricksConfig.cronSecret` | Cron secret | `""` | -| `env` | Additional environment variables | `{}` | -| `hostname` | Hostname for Formbricks | `""` | -| `traefik.enabled` | Enable Traefik ingress | `false` | -| `traefik.ingressRoute.dashboard.enabled` | Enable Traefik dashboard | `false` | -| `traefik.additionalArguments` | Additional arguments for Traefik | [See values.yaml] | -| `traefik.tls.enabled` | Enable TLS for Traefik | `true` | -| `traefik.tls.certResolver` | Cert resolver for Traefik | `letsencrypt` | -| `traefik.ports.web.port` | HTTP port for Traefik | `80` | -| `traefik.ports.websecure.port` | HTTPS port for Traefik | `443` | -| `traefik.persistence.enabled` | Enable persistence for Traefik | `true` | -| `traefik.persistence.size` | Size of persistent volume for Traefik | `128Mi` | -| `traefik.podSecurityContext.fsGroup` | fsGroup for Traefik pods | `0` | -| `traefik.hostNetwork` | Use host network for Traefik | `true` | -| `traefik.securityContext` | Security context for Traefik | [See values.yaml] | -| `redis.enabled` | Enable Redis | `true` | -| `redis.externalUrl` | External Redis URL | `""` | -| `redis.architecture` | Redis architecture | `standalone` | -| `redis.auth.enabled` | Enable Redis authentication | `true` | -| `redis.auth.password` | Redis password | `redispassword` | -| `redis.master.persistence.enabled` | Enable persistence for Redis master | `false` | -| `redis.replica.replicaCount` | Number of Redis replicas | `0` | -| `postgresql.enabled` | Enable PostgreSQL | `true` | -| `postgresql.externalUrl` | External PostgreSQL URL | `""` | -| `postgresql.auth.username` | PostgreSQL username | `formbricks` | -| `postgresql.auth.password` | PostgreSQL password | `formbrickspassword` | -| `postgresql.auth.database` | PostgreSQL database name | `formbricks` | -| `postgresql.primary.persistence.enabled` | Enable persistence for PostgreSQL | `true` | -| `postgresql.primary.persistence.size` | Size of persistent volume for PostgreSQL | `10Gi` | - -This table provides a comprehensive overview of all configurable fields in the Formbricks Helm chart, along with their descriptions and default values. Users can refer to this table to understand what each field does and how they can customize their Formbricks deployment. - -## Full Values Documentation - -Below is a comprehensive list of all configurable values in the Formbricks Helm chart: - -```yaml -image: - repository: ghcr.io/formbricks/formbricks - pullPolicy: IfNotPresent - tag: "2.6.0" - -service: - type: ClusterIP - port: 80 - targetPort: 3000 - -resources: - limits: - cpu: 500m - memory: 1Gi - -autoscaling: - enabled: false - minReplicas: 2 - maxReplicas: 5 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 80 - behavior: - scaleDown: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - scaleUp: - stabilizationWindowSeconds: 0 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - - type: Pods - value: 4 - periodSeconds: 15 - selectPolicy: Max - -replicaCount: 1 - -formbricksConfig: - nextAuthSecret: "" - encryptionKey: "" - cronSecret: "" - -env: {} - -hostname: "" - -traefik: - enabled: false - ingressRoute: - dashboard: - enabled: false - additionalArguments: - - "--providers.file.filename=/config/traefik.toml" - tls: - enabled: true - certResolver: letsencrypt - ports: - web: - port: 80 - websecure: - port: 443 - tls: - enabled: true - certResolver: letsencrypt - persistence: - enabled: true - name: traefik-acme - accessMode: ReadWriteOnce - size: 128Mi - path: /data - podSecurityContext: - fsGroup: 0 - hostNetwork: true - securityContext: - capabilities: - drop: - - ALL - add: - - NET_ADMIN - - NET_BIND_SERVICE - - NET_BROADCAST - - NET_RAW - runAsUser: 0 - runAsGroup: 0 - runAsNonRoot: false - readOnlyRootFilesystem: true - -redis: - enabled: false - externalUrl: "" - architecture: standalone - auth: - enabled: true - password: redispassword - master: - persistence: - enabled: false - replica: - replicaCount: 0 - -postgresql: - enabled: false - externalUrl: "" - auth: - username: formbricks - password: formbrickspassword - database: formbricks - primary: - persistence: - enabled: true - size: 10Gi -``` - -You can customize these values by creating a `values.yaml` file or by using the `--set` flag when running `helm install` or `helm upgrade`. - -## ✍️ Contribution - -We are very happy if you are interested in contributing to Formbricks 🤗 - -Here are a few options: - -- Star this repo. - -- Create issues every time you feel something is missing or goes wrong. - -- Upvote issues with 👍 reaction so we know what the demand for a particular issue is to prioritize it within the roadmap. - -Please check out [our contribution guide](https://formbricks.com/docs/developer-docs/contributing/get-started) and our [list of open issues](https://github.com/formbricks/formbricks/issues) for more information. - -## MicroK8s Installation and Formbricks Deployment - -### MicroK8s Quick Setup - -1. Install MicroK8s: - - ```bash - sudo snap install microk8s --classic - ``` - -2. Enable necessary add-ons: - ```bash - microk8s enable dns storage ingress helm3 - ``` - -### Deploying Formbricks on MicroK8s - -1. Add the Formbricks Helm repository: - - ```bash - microk8s helm3 repo add formbricks https://charts.formbricks.com - microk8s helm3 repo update - ``` - -2. Install Formbricks: - ```bash - microk8s helm3 install my-formbricks formbricks/formbricks --namespace formbricks --create-namespace - ``` - -For more detailed information on MicroK8s, including advanced configuration and usage, please refer to the [official MicroK8s documentation](https://microk8s.io/docs). - -For Formbricks Helm chart configuration options, see the [Configuration](#configuration) and [Full Values Documentation](#full-values-documentation) sections of this document. -``` -```` +# formbricks + +![Version: 3.3.1](https://img.shields.io/badge/Version-3.3.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.3.1](https://img.shields.io/badge/AppVersion-v3.3.1-informational?style=flat-square) + +A Helm chart for Formbricks with PostgreSQL, Redis + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| Formbricks | | | + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| oci://registry-1.docker.io/bitnamicharts | postgresql | 16.4.16 | +| oci://registry-1.docker.io/bitnamicharts | redis | 20.11.2 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| autoscaling.additionalLabels | object | `{}` | | +| autoscaling.annotations | object | `{}` | | +| autoscaling.enabled | bool | `true` | | +| autoscaling.maxReplicas | int | `10` | | +| autoscaling.metrics[0].resource.name | string | `"cpu"` | | +| autoscaling.metrics[0].resource.target.averageUtilization | int | `60` | | +| autoscaling.metrics[0].resource.target.type | string | `"Utilization"` | | +| autoscaling.metrics[0].type | string | `"Resource"` | | +| autoscaling.metrics[1].resource.name | string | `"memory"` | | +| autoscaling.metrics[1].resource.target.averageUtilization | int | `60` | | +| autoscaling.metrics[1].resource.target.type | string | `"Utilization"` | | +| autoscaling.metrics[1].type | string | `"Resource"` | | +| autoscaling.minReplicas | int | `1` | | +| componentOverride | string | `""` | | +| cronJob.enabled | bool | `false` | | +| cronJob.jobs | object | `{}` | | +| deployment.additionalLabels | object | `{}` | | +| deployment.additionalPodAnnotations | object | `{}` | | +| deployment.additionalPodLabels | object | `{}` | | +| deployment.affinity | object | `{}` | | +| deployment.annotations | object | `{}` | | +| deployment.args | list | `[]` | | +| deployment.command | list | `[]` | | +| deployment.containerSecurityContext.readOnlyRootFilesystem | bool | `true` | | +| deployment.containerSecurityContext.runAsNonRoot | bool | `true` | | +| deployment.env.EMAIL_VERIFICATION_DISABLED.value | string | `"1"` | | +| deployment.env.PASSWORD_RESET_DISABLED.value | string | `"1"` | | +| deployment.envFrom | string | `nil` | | +| deployment.image.digest | string | `""` | | +| deployment.image.pullPolicy | string | `"IfNotPresent"` | | +| deployment.image.repository | string | `"ghcr.io/formbricks/formbricks"` | | +| deployment.imagePullSecrets | string | `""` | | +| deployment.nodeSelector | object | `{}` | | +| deployment.ports.http.containerPort | int | `3000` | | +| deployment.ports.http.exposed | bool | `true` | | +| deployment.ports.http.protocol | string | `"TCP"` | | +| deployment.ports.metrics.containerPort | int | `9464` | | +| deployment.ports.metrics.exposed | bool | `true` | | +| deployment.ports.metrics.protocol | string | `"TCP"` | | +| deployment.probes.livenessProbe.failureThreshold | int | `5` | | +| deployment.probes.livenessProbe.httpGet.path | string | `"/health"` | | +| deployment.probes.livenessProbe.httpGet.port | int | `3000` | | +| deployment.probes.livenessProbe.initialDelaySeconds | int | `10` | | +| deployment.probes.livenessProbe.periodSeconds | int | `10` | | +| deployment.probes.livenessProbe.successThreshold | int | `1` | | +| deployment.probes.livenessProbe.timeoutSeconds | int | `5` | | +| deployment.probes.readinessProbe.failureThreshold | int | `5` | | +| deployment.probes.readinessProbe.httpGet.path | string | `"/health"` | | +| deployment.probes.readinessProbe.httpGet.port | int | `3000` | | +| deployment.probes.readinessProbe.initialDelaySeconds | int | `10` | | +| deployment.probes.readinessProbe.periodSeconds | int | `10` | | +| deployment.probes.readinessProbe.successThreshold | int | `1` | | +| deployment.probes.readinessProbe.timeoutSeconds | int | `5` | | +| deployment.probes.startupProbe.failureThreshold | int | `30` | | +| deployment.probes.startupProbe.periodSeconds | int | `10` | | +| deployment.probes.startupProbe.tcpSocket.port | int | `3000` | | +| deployment.reloadOnChange | bool | `false` | | +| deployment.replicas | int | `1` | | +| deployment.resources.limits.memory | string | `"2Gi"` | | +| deployment.resources.requests.cpu | string | `"1"` | | +| deployment.resources.requests.memory | string | `"1Gi"` | | +| deployment.revisionHistoryLimit | int | `2` | | +| deployment.securityContext | object | `{}` | | +| deployment.strategy.type | string | `"RollingUpdate"` | | +| deployment.tolerations | list | `[]` | | +| deployment.topologySpreadConstraints | list | `[]` | | +| enterprise.enabled | bool | `false` | | +| enterprise.licenseKey | string | `""` | | +| externalSecret.enabled | bool | `false` | | +| externalSecret.files | object | `{}` | | +| externalSecret.refreshInterval | string | `"1h"` | | +| externalSecret.secretStore.kind | string | `"ClusterSecretStore"` | | +| externalSecret.secretStore.name | string | `"aws-secrets-manager"` | | +| ingress.annotations | object | `{}` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts[0].host | string | `"k8s.formbricks.com"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"Prefix"` | | +| ingress.hosts[0].paths[0].serviceName | string | `"formbricks"` | | +| ingress.ingressClassName | string | `"alb"` | | +| nameOverride | string | `""` | | +| partOfOverride | string | `""` | | +| postgresql.auth.database | string | `"formbricks"` | | +| postgresql.auth.existingSecret | string | `"formbricks-app-secrets"` | | +| postgresql.auth.secretKeys.adminPasswordKey | string | `"POSTGRES_ADMIN_PASSWORD"` | | +| postgresql.auth.secretKeys.userPasswordKey | string | `"POSTGRES_USER_PASSWORD"` | | +| postgresql.auth.username | string | `"formbricks"` | | +| postgresql.enabled | bool | `true` | | +| postgresql.externalDatabaseUrl | string | `""` | | +| postgresql.fullnameOverride | string | `"formbricks-postgresql"` | | +| postgresql.global.security.allowInsecureImages | bool | `true` | | +| postgresql.image.repository | string | `"pgvector/pgvector"` | | +| postgresql.image.tag | string | `"0.8.0-pg17"` | | +| postgresql.primary.containerSecurityContext.enabled | bool | `true` | | +| postgresql.primary.containerSecurityContext.readOnlyRootFilesystem | bool | `false` | | +| postgresql.primary.containerSecurityContext.runAsUser | int | `1001` | | +| postgresql.primary.networkPolicy.enabled | bool | `false` | | +| postgresql.primary.persistence.enabled | bool | `true` | | +| postgresql.primary.persistence.size | string | `"10Gi"` | | +| postgresql.primary.podSecurityContext.enabled | bool | `true` | | +| postgresql.primary.podSecurityContext.fsGroup | int | `1001` | | +| postgresql.primary.podSecurityContext.runAsUser | int | `1001` | | +| rbac.enabled | bool | `false` | | +| rbac.serviceAccount.additionalLabels | object | `{}` | | +| rbac.serviceAccount.annotations | object | `{}` | | +| rbac.serviceAccount.enabled | bool | `false` | | +| rbac.serviceAccount.name | string | `""` | | +| redis.architecture | string | `"standalone"` | | +| redis.auth.enabled | bool | `true` | | +| redis.auth.existingSecret | string | `"formbricks-app-secrets"` | | +| redis.auth.existingSecretPasswordKey | string | `"REDIS_PASSWORD"` | | +| redis.enabled | bool | `true` | | +| redis.externalRedisUrl | string | `""` | | +| redis.fullnameOverride | string | `"formbricks-redis"` | | +| redis.master.persistence.enabled | bool | `true` | | +| redis.networkPolicy.enabled | bool | `false` | | +| secret.enabled | bool | `true` | | +| service.additionalLabels | object | `{}` | | +| service.annotations | object | `{}` | | +| service.enabled | bool | `true` | | +| service.ports | list | `[]` | | +| service.type | string | `"ClusterIP"` | | +| serviceMonitor.additionalLabels | string | `nil` | | +| serviceMonitor.annotations | string | `nil` | | +| serviceMonitor.enabled | bool | `true` | | +| serviceMonitor.endpoints[0].interval | string | `"5s"` | | +| serviceMonitor.endpoints[0].path | string | `"/metrics"` | | +| serviceMonitor.endpoints[0].port | string | `"metrics"` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/helm-chart/charts/postgresql-15.5.36.tgz b/helm-chart/charts/postgresql-15.5.36.tgz deleted file mode 100644 index 925454fefe56108d75871dd69ecf13169d659042..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75785 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

y0 z6NKX98(PUvW^I#-?x+QKi|*eo4|y~fX0KUrJB|!V%pc^k04G=g)KpIL$%r@%^y^$f zL@NR|Nhh!LeI+lS6lVudOZCi#^Z9#cyeuCa%plELJ(2_r$5k-TJIa{yKL(SF@U%84 zO+Fhk_Ft%?LuuhMDQApDc~wYAwdJb_7sO2);omuFz`wV|@p4k^|cfUdscs=h(B+M9L2jEVG?(Q==)7Y(q&)V!CpY}){|7qHN zW9Hxg#;J9BV7}YnL@4Go4=n`GY)b@F@J;E#4PN=RE}o{xsppic-E~oJZE3QuCL4K< z{;N5mc^LW*l=KDhPyFW>U)m6f5MqjYkU33~Og-I}ADG*(Mnqr0Y?j ztE|UH2Kpc8r$Eq#Z$F5MqhX^%7(=sflwXpiV2Wcpq8GA~C4#7?oY{H)aS|!0J>MxJ zTC3PrC?xz-7NpC?^QyZu5Kg385H$)KeHg+x`MW)%f4}}>lDqs*iU*jgCNH;6r1%Mk ze2?gNg>{o*mCQjz!5CDa;9p`E5x#H`T3J+j>$0+-lUsl~Q~TUQyOfd5&pPMBzth39 z=nylEGl%aGyv8Ek@=HK8Kr7rb@NG;s3>?V46dhl?Dq{w@w57{8mK%CjZ{}!P8iiE~ z`d*Snx|{M>afcDey`(^Tw2-^4PKT(@5)*nH5S-#)AGuQP8NY^gS9Vv56myLq6@oC| zRM)Oc!@9#JxPpaxtJvNIofPd82B<5+M(Sy|Cs?(R0)j5Smg{3r~yuv~rg9(D%ub0qhNGlYV zYh)k6{G@~xg^knhF)e$$-+XR7Lvy^Y5>yss)G5$E*ChGI=T1@{Qn$cy?c*3#^m{#6 ziNM`L527F#2IkRqI0(Ncl${l~c^0zT>~ZM{N#dRopIVel*yaA)=yvarhM&gPB*_gq zPl$XJvK{zvVHkLS7*Pcwa_{!@{rvp+wJ{jtUw7U49bL$RdP8k6_`+lXOtd*XOgX$GJe3X7g#KO38-Nv^hBjKaw5VnYXnPP3eBorjhM!7@wiZA~&yN;<^!-Bq#xwO@gL@vo0a|@OPPL{C9>D8gO1mxn(bRyh6Cbz`#+c|v zDMkaqQBhXXDZa%_hu|zZfwGBjy}CrPMX2`xUtKgD$z5T+&QK*bJDVDRwIEORrr_|c z(eZ$J`HcNI-@CYhU5{WE`?2s~230$*YG-Q-rs{Yh%Qn<>{_iSSSooWZDh5SH*ZtV2tVoH6Erk%?Mpva zueu;QYJ^KTy#N~`jKkV^=&aXP#(GhrzTxmxB!cmk8tGxP_qGJDSXvZF0Jy(A zWqiKnwAk--M*&J+d8XcLD8G~lOG?*8sdc-G(sIYuZLFn6sc7?tmL2rVTGX1Z)7q?L zTl}cx8IH{w7QPo6<%c%-UVYz+q)Ktm!8pc=@Y@L%=NV zb&)|b;Y_^}|N2=nto3zM9IfkxW-$*4^Zwxc@~`c9ssHwlpINv8B%Qp_$|LBQ?3GxDQ;2uo8I;Hxx4b;c1i&V5zm0VzP-rrADH68 zg}-x(Cm+xtkGj^4&`wuRd;W@pzRTxI{sH@+ZtX3Xe~VP{Lv6;YM?{8u%pNfI5rT&U@#E=zY(Lez0mX{>}+anuCSAfSMU_7 ztnW(E#EL^3Y0bUYW1yYHht&x%De&nDjdv`R^YmFwv+^=8oO!5yP)%+Kw=Z$}dD?2y zX`R}2A0=yE{kl~9=d;W7^*4W62q5fZ06Rp7{rv-AV*nknli-u>_RSvj^p<`7QILJ+ zotz;Q7%K^{fws5AjH3Dua3fmpG)yHZD z;8^>9L~r{E@P>GM(6@Kf-{;Rcd@x-G2-QCRi)^F)Q~b+Mr>=g5{6o?q5L5Q2TIc^$ zIvm6|o&pS(yuqo?ykY(7eXJoIg{P=R(ZI3S|67&=pwtqFbTIzB@!JOY-@fmf z0sxYH`gGsy#ium=pM`{AtqL|(ioII>`XB*4`Ihd!0llJ#-r>KdeXX8)woh{6Af1I$ z^q|>J0Rsa#LPL!*Kjr8rC{dB}Og+9oGN7JOthsez z(kOnyk@@65ixYu=xAmw{?M6fDB-L^(YS#lbrerZC!?C(9teU6n$kqj2SYc|Nx4*A+ z6LbXZ`Y#jPuE?g!xC**ura587-^e(Q<|{QU9@V;G2Y-pZ>~9Lv$Zfg0G&-!bYszYT zpp9LE%>R<`R9y1d2`l`{BrVIAcxH3uSNj=sS~J6KvK<%{W@~}P@|EtMQJ3gK3i}%v~hPW;o|vWgyl;@rUs?( z^Q2sPD$o$$YQhB&XF+opO~ls67Uj>_h#@`HZA48I4mvs)t2{a>Y9Pyg%g+8*6e$nqS9IIp#vh5pIJBFraY3KP^sjsS$2ygLYYfQ>;HT~<-*Mj9Q zs~BpARc@IC`;8rIfI-aPQmNy3GZN4_IIlL7EZ!|2UwZdEX=EwR$pz8jS3DbvN7RDtxV=f;FtDp63K*PbCj^AWj$x%W3`mxaY-oYD0tS`?&Ia z9oD!vy{0wBePMEau2OQV9e=s`l@=clPt5peb+(|Y@~YiSMR(IAUJq_w4J%hN1b=#J zl;xICgDll!jo??tz@f)}gf==-$VUWhCEHH1DbfL_fVhqNOYj7KN;~PNp*|^KtEh)s zWo5XLM+AiJuc@}`*Pcyyu&cQvaY7=(!cVM)up->~&orxXgoGsFjiDG4nmg1Hy3TpS z$288hsUjjUf+3t#3sYI;!ArP^DbIuRRH8XQS; z?Inm=dlJyI+|a%(0w@y~gOVt#UApR(iQUP6v|m&sI~E^aN>iZoXIEp5&vG-Y4JQTV zOcMzCeHUeBnGWPJFyBQyDbcH5?1~!t98E)h-U4!rTrI`9R@K|N>uk^YUIR{f(_l$x zo4c-rN@*aQ$Blv-K2o9QP;I!BQ?@iq$M4DTG7^F&x8oag(A7$)*)UP2o1X&Y_uD$p zx4RGYw8mY9fq&KW=+F)VKt=y|2vB7X)72;{5lY+Hp=f+)zKzHo>j z|G0A0f?ymVJ`tF=gu+w2am&^!j`G#V=i1DoXC#}gV{L(bsjd; z1G+(bK>7E2VFVyt{;+TN(CUI}R!O3YczWxYCq%Y`IYpxRes9tfm$j;4G`J^{Fus0z zF0mb&iusti0~hy3q6cgtr`*3v6WXU<$s|!+ zdT(~|_0@=hZV~69K=GD{7dz}05voRfFmK5hfNaHGuVAEW&~0MN5s!6)e#XQ=y+7C9 zHeA|BSzS@IL|Q{A-$c%GFm+m;sIJUY$`R{GiR#p+MJp6ZwK=r|(&$wtp%s#A+IZ{N zohiaJW8wcr-U0%_P!D!qq1D!}CJE;m(5)h#$57S^1*&q6c|ysLJ2coUlg^w7>%pU? zIjqT+0=l3J6kYV~jD7swoPXjB&wGcDA1^mf>v~89*WH3FfiBg~Vu8~bp|Gwv_Eq!j zz*wXSq2HU;C4vG+>|1-ujPk4K^S@J@|=oMluckku)N}62&ut5_Q|>+ z#i~#bXNkppctrzTbKx)Ro^3k=NSRlai7|nJIqRBXEhu5o|C%p-2pp3igOOcdh7K$^ z-%hT+_j_Rt4+~Om>A^Ir&+y85ab5f?Bo+aEu;QT{O+wVBB5UJ$H-cz%U4QdZ_z;=&p}UNtea{O6Cx%5N@Q z!&XP_EV7!y*$MS!CSyiHv(VSI1a50Q_a%uUB!(jf zAk&ZDdA@~N*aqK%f$B~lvdfc9f0_O0^8WJp@B=}7ZLj!LBLOR_#)*qLnIl799Ep(HfGXz;ZtDaq%!7`UV#ISMv-_ z+t}K=8iOjdWQD)w)HhtNc+^sb61@I*wVT5M7G`>0 z^F{5V!`qSvfn9pSx2f!{-;!6+3Yv&eY-N^{uBkDdXbX3y(p(tq#P00>q3dNzm6MFG zklL(PL-&(_4-rDl`>QYrmc-naE*w*7si~j~&u`|SZjKsN>3LcxP!4u>G)7l$iUN0!S^71;wMiZopOpE=9j0B^X{i1NwBy@ zP}NSS(#d9H?%wh}aB8=LxpTOgeVryeS!R0N1^xXZ&y}(jvyxR&VHKg;9StjZE?Z4_ z(s(Hv<{S}r2l65E?=WSdp#tk%lRFs^o`VW^ivD~`iYg18Kt8zK(}uLI+`O*YI~+UA z)j^_8%-wZ41?A>Y$+_&}+VbXL>}Wm+l>~9F`_M=iNA`CW54Q?ziMNxdYE5S8OEvWx zIV$hh(_xSLS?T*>_D@Jbar^2s5ELaD2dtuZR@2KS3!0(To5hnw!=*LFTsr3E-o~`( z0%kVcck*ODNgls&_r?VzD1XRJU|49gW?@d%W%0|!(Q&?u>&t}^Fq-wa)O0^cx4d;{ z>0gEy!}xIQt~D!szHZJT!b=L?2KR9JxId^mN_AmclwdNyi<+IkBXJ77QL*rl0C+ev z0?w4=(ire4s1vznW^%h5ew}^u@$n)Jz2eZScw2fjUE8kd$f}rTDmWE_4?6Gb1j=bO zpneFQ=jG;YYi;M_?`q}aKYQWEm(yVn8SnOr9&jGSz$p-QGXfZ&+#hNXysni@-#x9J z&7@>aD2G)m*RSRXM110j0tb9!;wecuK4DO~kWz^|dVKtv4;*&D-Ihyx#_h^J$DNe= z!3<})%vkGJoT~o{E6-QB2CBLCbj;&TKq^&=SvDVgv(C(LM9BET;3BBVe;feD~g}!-1EoZQLTA_LC z1oaf~PDA(XCYjeKv=J&TGh1MNO;>Z53GB@8`coz+ZhrkrdhuV%NLKe~4W!j;q6CM? zzhgB+5%dSw%DGCUEA!S335x6FI6@fe_Ry!r(jHK7XS6OTTEyc3G7j+2SkOLxBTh^9pTG!pw>b*=gv>%-$?Fq&e$WIh9>@Y#X>+wx35ZL5ZvC_EW^~J>ci>q^CvL zO_*s{^^{NZ@&Xp9bPI?c-v$?0k)OidgDhY|GN(wL9~DCg)hUb4!MByL zDvCz*m3*r~y+az-H(AI1*v+%0Yo@u9$+(qDTs2)4m)nUb60(FL9I4)*nWG0B2~)(E zRH}fW+jwx6lyzCal0SExDq3QldstJHS`f&s%r{?CFe-y``ap*(6ogJn7*cV8<%}B# z!McSnV{J`RO#cR`OM~U#jS#vNAVa*OHnxudof&3(Jd&PV0%N?EbK4~j= zghtAh>EJK4KtsCIam`e*2Yh#&+j+vcnW7{gOsk%>1I`{yUk=|Njx3{DQEKGK)<+BY zKf)Ov^rL<$?-#uAnHiDYKbyFcC}*L`c-^oIcAQpqMu1t$m#&<>upcaY`YoD*`jxNn zr}S9sMFc0k{8QfQhRO1vP|urxR(D1%ccf2qz-x+70xNZ;qhydVB}`48n|$&}H3@$( z_DEfPYz_1!kZb^);*pv1(wk!L>MfFQ!|i(-{r(|%3Q(Z0QNY}f)FrJvwS~HV<2B#U zcc*As)g|Lgp=o99;=8@!PatV*TwpQmerSM9c>9-2+_Fs4XH=@W@N}A}tQ_g=;F*WB zS}J}S4soO~)u2(++vw4<-0dDOMc|Kk5k>VAuisSkG@#vQvm=Hx$({3lRa#)i~ zo{47U1?fR2`#dd|LuWPLKgljqt1kOWs5~YeDLgOo*MON7TFyZ|iMa>WgoK|)+4!eL z&6JrX;q_PdytHE$t9s(|%I00q>+b%5N8e)%jUj^G>bJXw7#^yxd%4*C!2Cs9d)DTZ zSoa~*acIjY)^tNQy$Pqj)05Q5ZmY|KO(T3bR5Cd(&ujs09^Q56o>cc)b6_&7_1C>{ z@!gRLq{6XIMoTA=o+tMBsT*KY=Ew@SI-s8dK2U-7?%HUha`{%z&_({1kU4N{z+VD54#!ktZo^$}ehBp>m6%eKdw?ef5}PhXf~S{6MeN{G zLD!0+cUxA$BMM|NPM^P|y}L?m(q^cj8zM+GL)BJ;+Whv7t;#*5Sdl=YC0z-o7t$XQ zH})2C6VK{&XLpt#q^mv}+l<)xR2W`4QQNQ#w^Q2y0|{QW7qp`I$%8k#Q`J}PtP(yM zg{#UAghNEoOznK^oxIRn%Z`qwe(OH(gm8E|JjUs4FX>ksTBYUErJudzC8hJ327E0D z|A*3pp}I^?HGZ#J%mk54`TN*ukl#PV32G8d@)8dwjH1i7x z{rL^44qA2sxT0osf#LW$jjiQqD3%dm%zlC|i7&c1oT|IeGbr>8*KLL%U)~2_w~Hn+ z$ai3)aYvIjKKP&4cDElbfR}?~6!3Asl@Y{`+wJ#pJypSOzm3!RzR)-EV>8$Tz~Gm8 ze~evb__G{%wQFkrHaJQU*%yB+7fF@^51zeUbE({$xHCKsxGuI+FyK~{9}2z*Q)|(V z-J|!2zkQY=LI`Y=+^^*WM6pLm7q6`Ak%Z-mXTgJ#FMmMC+~^pxRQxG*O=>7DYb(|1 zYSjuGMOvnus41GDJaUgT3c;wtM?cSWdC2a{wxku#`O^~j> z4OTe3^o4s%{zfZaJJ3>4Kr&E=|Or}%n7Y$YJH0=fnHgbU!tZ~_Vf5*aiH3i~7f`_y@KjspJuFQ{BE*f!A} zjxvyM4$<&$P(>bCh71DyD9EI$@O0R10s*mm`+oAjfqGsCs%`;xijbieBBeIY&%~nh zcx)S>urO?-c;nA1Enu*Szr%3u$iw>ny`l@+=WmP<5-f_V_@2d;*;UiRi&#aW2kS?O z<1)VvUYor=E`~;gM?FOEF*|1-DalO};u2S9`a{b~lSKcmVc)}2S8G@ zn?P~0H-nCRc3Y>;2#fW?C3jq}$PrY<4)?iY z7!jdk-xx-kwhm2mQ{?O~FNbC98|5lMglEjb44{9gZN8ZYSHlRX-%W`Y?$U~ptL}C; zum9MRhbFLY-g3-8Msw72*=e)wRB$&KVXeP`N_%NaRb7@?54LU5HqcM{M;HZ)otWJ3 z3V$wD(1EibXzoS6*r=tyo7=<7Q|;lgnf#9)^~9e^)F;qL>;R~$5+t<*!0++ELr^+)vjgK`A2{$x*9;-Pp7osh&pHT$>J1igei znI;(?Z73s&b7Z3DzL+rl%~9I^y2q0MT^K97;w@nJ7u8!knfB;2NAzO$BZDahfREQX zeFtCPRP)jG;zX_yQYcLGD|fUpD6+(4A+?iNS-o3 zTEc_}4g251?emW*cu%p?0LV>rHbllz6x~C>Si}IxD1%-FVt3}6Uw$$_omm$7Mbywk z^D=sy`(+N;k{soG`a{RVNOI)<*I)bY!P|wzzY*lUw=RRW&m7${|nn`%q!=QPA z`E{bmbMIY>dq(gN7=Bj0`{IZKr{~8G>;{qIoyyM`4rIjgJi%+6;*?ckq$f2+3QA)=QVW4 z>G;ZzV`+`)mi>!`InJycf@PQSVt|pcLoCt|L;i6&cDtK8Q*trYz)>gcYgVG2h2IQW zzgDDzmc{@&?|}2Gj84GIhWp0DPC;Tx31CVuq3K_BtQYcR+=y|6KtlNxRoN>S#^7c~ z!^|II8=@-uaQ*U3&~f8?lerO)_fVu$2)c(ez`0zn^9`VrYjgv!-RaX-1c*`W=Ks=9 z>R<6f2K1eFWYAV6C4FXP{!1v$iaE24RWOa1t)f$J$QKteRbuJ~Q}c+;fTgVMFP+15 zr4U#}GND9mf&}~Yw^ej`<-%0~(tN`)`PeFv!5i%9c?Iyc;1C{=iaMv1cA9o0IQ0gK zrnT%)BYK9fi@{Haon;2d(XH_AnZol6ZN+>01$jwGijN@lK)X2Z-7uL@1t_Bcl6E^f zC_(8+8hgDp;3kT_LE=8dSD#JiugCI@iAOmjJ`%@M(3oJtdz5S^Bi)w@%xR;y7|yZg zSmA=j?hZB=t-r}Q-QaDx*3;}n=z@4jW&J|wuIpgs9qH17Fz)d2_#BhDPg)LnT#6E! zGR;z+zJ7B5ghsP=A}QH)TD;arDH1-99Ano4*vu4H9W^);Yue^jXgL}?fhKt84q3#e z%0;Vz8^!7)zdgAcFHMljjKe8OE%WVWZ4f>QT4J+^twI7w6_9PU=VJ%ex0Ef%Ta+$! zkSuuj)#2?qNi-k^&&8g83uK^!cYm|3@(7g=g^Cld7uSfEy1nJ>d|Wn1EBSuMeD0-Z z{yDh->>M5V<8*qp@B;Y7JVgNd;F9yFBD4m)>N7fD*&5VoL3{P=bT1!Gw!Z#~urxDAFa)+WC@83(X{joVE>(41%!~&$ z-CmN8Bt`BDW< zQH^n^J;tG>q)zntzZ)4lgs3e!E>-y`Y5 zh3()G;z4JmnJw)T=QHtK+ZQ57E9U(c4DK+KlBkj z{wzZS;BoU2LGC8tAx<(9}4?Kkn_hZ-aVh_QN&sOeK49LEyA}8CLJUuoe8LQ z^YrD9?Cu1+y(UkUt6HjYS{cwe*B6qLFHK4a+e@$9m^wQ@F3rk~_~-=gcrxsW_SPp- ziSU#FS2lU-_@~UtTMcs~A#;UJ`xzxpfgTPR)fS^N{E8_F-LVOk){XglI}tlY<#Cj2 z6ID!q8X8&fo!lQn;WSW(dxxge?Ic}Qnl=Ac{L==9(M`?x+eSN{D$7qv^5`J>%uwb7 zNw@UXtfrtv)kODzoMXhphEltc3pQ^Q#z9l#2y>PpLCAMm_GXnVP(>xwXnd1PwGITf z@7UK-&)a?DlDUnl^1KU$6LyPZy(91`CBx~a`1fwBewZubIj^2r{{_GH;jhFqn<=n> z1fV${`zE|l{%L5T@)W;=5L9p<&8F!kNjk{e-6JS}P3eY&VJ~71kPlurZlm2oe3y-=0nmL9PuVtaPvSeHQ=Rp z3Tj4ynTnoyF|dR4bZPv_X4>@`n3FM7as+5}#OOcb`I-~LiOdr)y5kqF;sHkZ^-{+? zu`DEDfcQx*RNKaa!w09Gw|2Oc!`o>!NoDVuCDkHYjljW)eMp)3l;2a9BN7gTP>Kz1 z8gH^Wop$2&aOl7^D_7T@nuj;|qa+o4Q2vKZPn^Y&0@QzKBkl0y``X4JD~!V$Lda29QB@oMSvKWmf>X5>V#R-GN2&EDQ9tcVVPT6|=j zaVHi$5$?z0aD1<6Bg4*dbHqZ%i!GsVNV58gF9oADd=Wl8NMbT_50v=3uh?dM+{29!>HE8ktlF3 zuLH@VI4#jS9>@;qz(6m9)Oa}J0e(O)y^)+2Z@EzExvXBCrF?Rl%2F1>Z-w<#H8ud% zDelRu9ROcBj?P_X9%y2aKfs$-d7Ln?}R(Jx0&l|7ZN!(1Qs%pVfr?~7V ziAXpSWd}?NQ)<)=OGU2iKa~=lS0NO{H+ICOgjX5)_@$|rf$+?#5eKeZlZWpbkpr+2 ztYK-n(w?#O`DGe9^mXlM`v_(IlfZ)`4me1|b?$PX(CG$=aMWq~&bVi6=;;w^M=HHi zO)cL6DQ*v=)CBM-*1vjq@+x7T>+4u!T2}k02*men+eYsm&Xz<^J|Ev-4^L+Bopc1F zC5mJOSb|-OiNgbmCl8k=Pkm5X@8=lsW;EBf$|;o0WV+bW`0G80ixHn7;R?FlIGJ#1r-i+KpCpEkGy0{(+_GO6Hv|>KG4ek~-mPf9w5cjo z=Eb0>EyVMQ$DoJhul66|4Dft*|N8#p0whQ|q$dKYBE^q0jy(f(5~zUQm2uMh$_lh8 z3C87`lLqLp@95JE{uY-Kexpv&&6U2obxU`Z>pLVjO~9ouH7S4qiPL04Bu}rcmGtQ5 zWWH2?cc3wac>JY)Orse(@q-JfXAi-a^fw=Z1i>bp!=ptGR1B_K8$CVpnQLRX(& zb`pQQzn1x{uE`G#KCw34Z~T4QYQqUWx91xBq7s9PTr5Rt@AoZ=YKx3pZT(N=y+HE<|F3&ir6Dw z?OOy=XzwymhA!~7b(WJeJssmhpv?V0U&IUIIC(%g(VaW+Fcf}c=uck5<0==bfx}9Z z_y6yT9Mi#X@*O{jc|D&6kqFC?AVi>T0Au=-M6KMdM?|;h(9y}Y2cD?h6btF=Qu_}X zZQ;6!4Pw@b>yB1-s$csP+&E*WCSbVRusAFiFuK zJCN(eK;KJFw>sXL=Q1NFPgh+%BTxTxynjuHA+N~h)eB@E+~P2Tk66TItpZltcsS7@ z{hrI5eSF>CA52f~7xOL>X`abriF(8p1#YbZ1km1kwV?nupK9&u(vYH7xM#x8s6O1-L~vVb+ea4f0isw zhUSvl_1a29!rCsmd)H+)Bejaa4Gpy8yroSqfSctHh?X0*GHMr9pE(P8qP-GfAM{w3 zGhEkEBe$w2xXfc(`|G>zGu@c7Ws|moim!XxF64A(TUdC+BREN~Z*u2<>2+CphJ-n1 z`udLU{bo`ru2!&(=nJs$`InaZ2;0cF;TWlm99sn@Z`tDlcnCA9=&VUDuTrF-KUlvA zYBERd)gZuIeoW@{2*0tPwfR07yQ3ui{c}tHGmvt#4sjLq{4;NCItHC6_}EGt-0dvw zQlj1V8Ll_#R;@rOlb`eDXl3tdqYw>CjhY(tXmNKb8reQjB4(SXuT1lA2W-TY1 znXz9cTk!p!+nk>qDM(9!d~Xf3n4j!uQl8RTx%hc>reey%l)%Wb&b32)ySB(f-25SZmyixv1*BxRxOU$*HTqL4w|;eh&j-$* zn0c%6HvwHdh?17sE+If`HoUz@VzHL4cNv0d>5<#NsR|waxl)4uGcG*nPsenlB$p-d znx-Si08^cq?p!NKaQB1D8psMjgTabl#>?({tgx)^V-=$l=p>ldtC2 z&sNi7_&1~PWWe{{kmFDcFrrCC!tZ)vM1CdM`Yu0^Jmjfy%ev-ih30+^_R8|}C+_Ti z2%^KjQFhhVSwHJQ`E{{rlJ%tEI-1B(1i_S%(%h3i)Hxij6@NdSXhm%mpie=%(ilZi zHIkTKNuO9#Oub=OJS2)B^QcIuiG;)1&sP+GF_Kv}k zMen|FY+DoCwlfpk#>95Vww;M>I}@j4TN6*5Ol;@&|D1DA)vbCzz5QwLTHUL9cdfm8 zKhIBxlh!`C3-{Cx89GHxq9e`kk!7s(d~Z`mbsbB?aZR+zNx2H%z3RV&%NSAQu*Ka7}+7`xqcBe z?3w!pUMglW=i}?WWfK3v*nOu>WDrn5uCQX5vUF%e-eiaqB6vevh$2{ZjaWfEjC$&e z85F2~_)&u@W558ChOHalP z>1`SkEA;qSJqrIa4^nk)tiT0&?k@@rk<#cpwN{)5=Zfv>)Rfzb>HvM>XFoO4WiZYY z3z~{d%LN?}qZ0r|V$3=Le%`BT<(gA6J^eN3q1j-fvl6knm?A8;s(A4Xo5AeLivKL} zW##sKGdn$@c@dG$CozK!1pHn_uiH8ea8edrL4DeL+gR<-`x)NC5q^Ok()a*|9=ub# zbQa3v=44J0B+frMSbm2McGF3yoJ;G5U^-dG^JgBPW1_A^iDXC%EX!9IIzU~Mf3qU? zllxd3las7HevLJ6=PS0EHRlUpqHajU-}m@5z)bypZoRYaz2VtpWoBbayE}6 zis8rU?)BEjs^cdJA>dz({(lgHpR*TVODD-zk~f|Mz}ZyM9g0OECjxK#Id}*sIzv{c zD7>QfF-M-)p8dY|nT81QaNY8sJpbPi!Lv}5dY%<|1rLzOORx-%vlfn7^;>#Mb82WQ z2Q81CwM|mZ4d`1F$}7&?7J-9s16L4D_kVDMreLmv3?+ky+NDno*vi##r!KQB7Bio( zp3jM<8%t?GJJYUtgibP?0q19k!TyOKvSl_ozhnnynK3~ve~_c2R!euynn3!?c&Lx~)QhaIWi;LwV~=JhRLA|xIZDY>xP>A(LU zZ~*4_{}&Fxh_mnsGy!%DOZ>?@OAjQ2*!xcVdB6(tydytRaC_s03Vn^NVi96ro^BmJ zRf)R^zLvYGFonsdyjX6Lt7N>SFFK<>sVnc5Uoqw`}cud)C^oRKDAt4K2r=!-Kwd*ejH_+a1$X zk>Gc&$i=5vv%aDVt<+eX(wbn+oB4w-?4eF`w>&vH&$L?ubAK-(u^SCZhWg^_+4?0x zin%X*7}k5VH0RY^8v`^Fc<}N{U}*1ne*i>As>Ku%G~;!dsc z(AE}KQ>aw3JM=Fe+kUdaga2nvwT*D*DFAXz_L$Cyky}jXaD3{#MT?usSyk6evApLM zewjNpXGZ@xm*`?jvw6njPBIsu$+x@yiZs4tkvk6;kuf`EO?Kz>Nh_C5 zZV&njK{8TC1|-gElDKLCC%l5PdX_*^5^xm0IS)JVI7qh=`id6ny`a=CyQFf!UyA?q z?coa%UKVDTVl}#DZe)Gz#_eXJ}zcBjaHb7ZxI>m_R=dI+$s;gB#2&liRvYg{=0AP zpR)SZ=l9_-`9)GJi2nK2JkVB)cilY%CMNAx04ukm=WBw*Ap&7DZz@cN^;O0$vh^Op zd%>kG_>OyPuyZqT6MCAE(>C~9LO&QA>LF1nQ(Q%7SLV>PNf>47AGtg(lFQ&S6q&mp zNzdO>c&5GZjufrhW(O9l*1b+F^u~L(42-p7xEZ zh46a)E+}S=llf-DLvb5zcm#THry38RpNT;YiCogG{us|Hp?6o|dy!N#3#`aZJKbVe zz<7mSqqilVuki&PlKE2C=r+Y+2OWcl?8|Xi0ltb$jNd^3TeM3hjpGq!ewOU8C~m9t z&w8ER!3vi~JIJzU){auk^2=W=LyZ@m4J0XA^IHyH30Gp^6Al+?(^OU25%yM1JUh&5 z@G9m5%wy8tQrjzNDWzPa(|h>;HH`|Ty+3-xF7+{VPqg=l zdYs6}eDC*8BJ|h6jvy{g=u;XX;R|G=sbxbz4ed_k zO4pCNfMx9)ZW-t}Gh&G`XBn^Xos1=>|!JSQe0`J^XAnZ^zX6Gx0) zMZ=zYkfAUlp?4|TO`6Awaht61MmdDE2>qE<7c`EFmO2`oF$#CPi*LkL!q?93Y_-@H z_)(;;SEM3YrAA*I*oqjHr%4d}T5Sgx@S`~y-P0+KRXHy09(5ADftzkI`D+~$Oa6wJ z6026OQ`tQJ-Q)VmVEgN8S@YAU z8H%HzewVeMnij5~msEn3#dXiA86#b5Gh#++oglbiJ9@kQvU!es zyyod-`En$dA9>pgIHKu7ep(CyMe8JE9)&TR9(L_Cl^qGT@|Mq zSduY>ltY}~ASqCEU^KQL$Nh-DskNhWTR(yrSZ>Nn9+Y4OsWjS@0*^FGOS&)lWO}>@ zN((3l8H9eB&4jOf_6eR6<>$AQe|ElAY<)Ry_xQhD+B-SZv>d?aXAQ#E@s zyvX{HJ$^mmTeW^R&wGOVYqk+ez?5)^ziJelEU&Q%$992nfJumwV?nq}prD=R?Wi6E zg|bQ`k?m4bdKB7a6oFL}5_6ockqU$J9$3k<<+qBJ`*?5Oj&UNeBE1mz3zkY!h@%rn z?7~d15kKPs$u=O{@wOU;`ss)h(9N4vsNtee$z=r(M%9(uMpl*D#la*!0%L1VKU^&b zyxdy)0z5n{yNuBJx9vIWXz%B-76<_)bQGW*C+Q@?Pq_7{%%E)4a1O{x?%!FYqUbS+ zG%{351Q2T24a6_h1eEx3XxR2qfN_Hg-Mv3BX0qF86x64Tk(uz2e9*#T6ZKF^#iF9_ zFuPWvkToZ)h2n{%sbur6UGttd5c;_vJEOniMvPgnrN^WL3B)Dg@2iA;N1A|#l&5qvV9pQ<@yKwarjiAHw+^QgZc zxW9`yt{)~lLeH>~0%F8yWPP1Qab55d8+EAxEsoyUdso5;pfJT@q>D?LK=>OZ71Tj6 zQj+S^YU50paiQfrh(YVIlpsz5m!Lzt{zSAH-Y|4XG~a2{7`zd@I~l>$V%E&|UNgDz}#EQCbK|+_Kqb*`jX|^OtK%$jtHc6rX=o`r}iPtWkhoF-N zn!FuW_i^f01!-M;S&>8<^{`ANGR>4m5|EodnlZ*30_88SoxYRpbEaP|5;z_RvoVmt zE?em*nL*0vr)E%E);~4{_X;wgE_~zW)iowD%=6{FFf#ajdH^+M&Ax6+o+<;pAI9Hj z9D!d@xS_=q*E^IjWa7PRF^t6Q@m^Erq}piEa}tVzl@?sBcH?pIt<>I*<4(eTT@(6 z(>?Y8Up`343-KAsrP)ZndJNWI9{W~CUU@4L`{T5nf#~CY5uJBX9vWu_Z-CPpPZ|ls z0GYDR0y8(tYr`Ib#5od@TG$Vq`N8~&awGU%xL<8b$0T8pl++`TjQNGMwvzs-4bMiF zY?!nmp(ZZ*(hpuSoKm1gn2-|p6_y^FW9^1|tV<>co|g#5svY@tyx?A&6*7_y=xIVK zmBLQyBz2-7T2L=JWxi@BzSsXNDajAdIV>7u99m+8;#_73D`O*_FaW`Eh&GIqG!$h7 zEEtKR({fsQHt*1)`1a|PuTjW}2e|#+J4KOflo%;EGti}Xom7nB#4GeQYxUKW@`(zT zK8Dmbq%Jf$E5o05?xos_<~F)UHBZ` zbg0G+O}*QHR4TDYawIv54_<{MKIX_Bju*WQdhy5Iu%LU|JfTNMZg=D~n# zX@kidXk7apUbOW$H68*^+^xL5#!d`m^tz`64y5>$xK zwSfuIA=OC4^E)mCZT?_FAhB}fe!-(umf(w8gL7i9;Dzo!j;8BoZPAI#@O*66r9!fJ z@DPkIN;J?xg%mzz4Ox6(Iqn2Ar%Y6s;4d0oQ_4NL)p6prjESfWC)@a*Unu!^_h@my zxQ%<-G4D2sYMpVSJJ-WzX~ntG_O*mz8(aA#MrrbBQErzQTCPoT6}9Go6zk*iiO8S* zr5`qR@{%}b8Lu|-dumYzr?4{DQZSEV1YoFc5R&W|33#njD9XZ?MP2DhSMma~+{q_d zaBkAAiEe42gd&t2xP<@)D%;(txM>XFurK*8DUK9N9y3hy?MJ7Lf#cyLw-z%zZraya@#E;GKGh|KECVX>wJ`!y&e*rQsdOLt( zfsMa4z(mVC@e*sZ!ytY5k1zs(Y?;rFg)G`kAjwH;Zdo|YNo*S5E=Qc*sWJ4O?|ELKq-oq5+i~X6JPp11HVS7 zvyIp7<37btrW?`6@%$IYxA)@$Wadcp<;>-KH1^aMCTz7)+_as&xu?N>9B<4~jc^b>`_+2ApN2d5MXS%0drs1@5x+=3J(UDEuc*^3>hGP)3 z?S)ey52glVI8{f8gTbmgul_o^*JX)Rngk582%Hw8D`-zK-TfYB8dwG&Qt{t1^S`I)ze5+ z_{(&~TG7bp*SI^Us274kKgnYOb0~|TCnP8+p2pfXRKdwW2`HmXz2NLp6;U{EC25MOMI znFE$ZSemKs3}(3q2KhMn!@O`iIloxFOrZm+#$HR(Z3B)ndlPw|+Cma1*cqMGln^r8 zC@Y#L3^0GemRG1rIu22L0WSlE^W;U%C=uHnxd7&=|Oa3cWL5pXfN5U z)tB3cTSshas%;;X(D#7PhT6U{IB6V1%Q-W?8fogXIdPAZNW4EP)Iv4jLj9-GOg~

h^Q|@#x$J+YO1DNTo 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 diff --git a/helm-chart/charts/postgresql-16.4.16.tgz b/helm-chart/charts/postgresql-16.4.16.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b862ff5d0d42b34d83ea4f479ed90070f1e4ee16 GIT binary patch literal 81305 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�A2G6a$$Xid?MMycs>c7MYgRtos2Jo@jRY!$ztlvH_IOChRL12es3#sE z^QSZKF3el||<%fH~?}=7^5w*H0AI(NLjhY{y51?I`uqngEYw zWi^2ESVp1&sK-(g_3<7p8|WGIvC5z|0?5@yezYsTS@=ggnrmY~Rv@z;5M%{H>!U&1 z5o-Vv(y~?$hYM-PzypIr+A*;Ope1wUjfja-Q~rJtCiDGO1T(2L+B49k=Cu70Lru!QKjnCn#TDB$;$#{33hbo9 z{NnkdzNII%#ZrJu`Iz>}qEyObvMN-itkyjluu7@YC5V+-740x9i^?_v zSL%uUmZ2;2o~#dsU#U5!u7qG|Vzn_COKBF_0$IxPXB{+4E&1OCo~6`=>w;S9oADXe zG7oDhFW${yTjt7qD1gg6p$(y27S@nLU6wHS@OYPbS-B4AWuahvys($@hHKN1woMuY*wjx0&R99b zreGLc17uUSmRccgW`x(r+BB1YAS_HH!AB2>Y2^G7qhgwQt^|##jJRiT%*}yg8hdcF z2$|-A)HqD0xyDUmWg4lh2bgJYr{4lS)I$r z0i;=v?{9~u`5r9?fV`GDQoVRV025e>a855Gttr(Aj(MOjd|IQmfE=R>q242%I3cvU1RWNTeb*(uEhNQ}h++ z_}I$|#@rXQ_;s#Pjo^y|Pz`qhQiHB>9dR*h&QYkTi>jJ-wn(8eIw&Qi%onJS)Lu$E zIjtm^tY%&^yNAU$H!W!Ms`HoudQG4(ir4`(8l(jr!FCm?vYFGXkr#aq zs#t2@l@EyJaz@kk03_!JaQU6mi{gfnr_VtKqW)bu8(@t?H7FgEQaW6u^yJVvUr_X` zVtVC(AH!S42#L<@O=x*6O2)i>C_?PXa@mx)H8Me2}f0$!vJ-I@?1 zwMo^18L3UIF4RaJg0F7Oa_-SI5&7U*wy<6?VPioWAhYP6+wV^CJ{fo%dp$#s8OMa#fN zIo;5Q-S|xiR~}q<5uvnciZ+No`UNfU{mXw2KOB8J9bO$BU;Nt1Xu2=A@uj6#|Nh-y z-S6h^cfR}GNB6taS3B)^0%=MbhnfOa)ss9fwgW09iKa`nIN_az-BKyo_9`vyTdE}~ z*EgF{JhB_LDRx1^j_Q&F(#ULCRJR~@cWd*tN^4ZB;uYJ>wJmqeR?_rn_wojEN&rLG z;nfZ@w0N`i1Yko=OmpDSq68(-p}2822_DMFkr_bLro$Qq5oI(5gwQ-bCmpB)YA8KY z5OG8KDFem;?TSxS>`?k5Ll2en?&^S)%DwYXjMo zd(P*f&W!yT&VQ(=5e{XYa<*(Zkq-Zd#8XN-r!p}`qCVvVK7CIPM`uVJeBQ~}5YLX` z#Av=smqHsmNUp8$bu9|qtat-lN2^7?BeWg}7PV$WIllIRN#W58+DCN#Nblvhd9}QUjU}mq75Sv0eaTMLJ6mDkt+; zrFIr4{y;rHr1ZUIb<*GbS%1@~{q)S#=Coh!3hlwYMMua7_u{#|NHeZ~ z`jU=k6_yU{ZE7lLy+B&kbPJuWtSDqoL-?P}#U^vRp`JnlEy?Q#m)jxR0qeG;4nHFc zXCHxlsZURSh$)UexRf8zuy>X#97qd_xy$75CDS<6f^=!JAyryoUG~hQp+_%IqVWXn z02dw^&;V)%e9-8_@eN9fwu)GJ+{O?jg16Qz8fW+fJ0fQfTly#niqvhSj)T%P4my)y z0n$A1g=DT5hn>4_dvCY%f^55Jlel+XcOIaLgu59QE+3A5XQ@D%e=YF0P`cv*AS%>7 zzo?g*P$}#quhyc zQL#|5=;nqTGN#n(f*ug{mq`(!bTI>sc~Vq2VlTV=O+^32Nml#SEumKYzo zLz;wN(#l5PHVm1N3=;k@B>@({fJfL243hWstkB=7&P<+(jnNhY!RFgSc)3BA*k(eB zZRs&0bZ|1gF;*Kbt;1rD4YE|LlsKCVohexQu$=WdzDoxkt zKndSIP(rrk3?pCb53~8zh6P(FP!h`g#C$Mc`ZN?5#%%jLJ)Ek%-aJg;C@VS*M31tg zQ$K`MmG&ajTi-9b$$ld&UR#eR(p2$AMh>kqEZdZ&Xec7B781@WbmZ~-R*V=hIdVlDS=JVrg0 zBoLyZfl4df^iM5zbz%m(R~VIry|-b=*#;#MywoEwqc74B-+l%w?T(IFRjV&%xCu*5 zSu2{bLkn4=Rw^=%5q-i9wXM#{OQHxM&a%tifB&)+b}!q~T`7K3s_mS?@h(($1-BRQ zm59$GubZ0pHxYpd`(c2u+Gx zs!TRK2PiEKP-@BqYR?c-~!Q5iVv}C|BfK4J{@ei_xpCLx@p803ow0RB-7Or#@Dc zlPtv{ye9gWP{g4_wmG=3qc8Xj2Q3X`|4EOsV)eQ*9OK}*6h~;=2*1!^d6k(z=&t7# zoCM)W8Q?k!9OlVOy_0*b9^uKV7m+$-lblwn09Ovp=YDx!@bMH{StqC{v=;u23b;e( zV({U3bb0YtyixjD=>l4i8w=frOLACAmSp1y?HnS7@bG+ee0feuccRoyr@?Gig$GtF zkZ^VO1E;q@jd1`88(tR1VG`{4wfTx{vmg~Nf zIP|ijIy$hv}|tjNt^RSpZAd^@HosTe#?3^5#GA z8`2Hg`IZo(;rQk(T4HrJbMV$|Um^4j+dT3x547Uk+m|&2^E%|-hRpqtjsuR)%5$z~ zl!W@wSQTKk_3*}uG$MD?mOOCLZ=vHPoZ|WY11eMvLwxu*g!JZbgLeF-anf5kMi_0oeL0bGvXACv=b4z0yV+RCqW2&80^HD zSaL>8N`=-391rYg6#yc2z6)*3GLC@_os`$%+`<0{cgJPLaINwUx6>P>z)r-aAI;Ly zosR0B6;RzOI~OfK1>Maa&|?tc2|xeLPAQAdALZjmX=8MbuX_x0dm-9vbesL2xRz#n ziarS!L>+lVB0br>S8X6+lEWGHnM~QRpA$_Z?V~A*4NO z!ceZs5tK$N0G?vSC40_zr4&V0scl5woXPLxU`srh6392%z@iC}&WD#AD#%C*ls62` zxS+mU7`REM!WxK(<{K;>VZ{bXNm)g5mV|WiNI~c~cL^5GL&@5TiKcCcIfxTW(Jj$G z3tbdMh3`Vk6^7SIu~~}xndh30B#agm8CI%qc7+K}5eN>i7&ix7#NkMuRV+-^BK!!q z5`W}(;5Z|s2vM4oseQV+f9=ls;WI6FT3aF~Bbpxe+kVazst z0$eq?_jf@Fh53ti`9hpDNC~gs^4HkWJdN{xEiO2x0f3CB%91lDPi3h|A7cbE|zP=ZBvD3y|h^@%yRx~xLh^mR5UnE|jS zSrt~<>?G5n)*}^ictchtnHRy9WL0&OAt_%fvzm;ksikovQEn(?)}q~Z_g%XyT8ws| z4Mv+W7%Agn43La)Z7IcDRl|B7|~i<=pmhPr)y zMu#s}Um*S+2X4&Xt0y_U7I?8`68Q*UDHXjnsJ20qH&PuaeKBzt!55bni}6pepF8S& zjA-I!%Mq=r$S1EYAe~SUOJTXj7v<1YKzryx|I+aRoEK}My%8RmubqBxG;n_UI5>k6 z5EXA0P1;j0<)vM5XVPt+{k8L|e|~m`ihQ->Tq>CbPOjjZXqIiq8Jr+$yUyV32snVZ zSJ(HjwEl2$erhPh!SO&6Z0f+ggRY4t*Y+!>O7B9TS@xp~KUh=0+7UFKxzc1rdBOU( z_y|6$6orB{7FrHg7jjWIyWWfCqPY0J5$Ndp+Z0d>Ff=wN7M!geaaT{1%CwUYX zW-`Iu^fj5%lAAU8m6BWgN{k(a^)ABL(OCcbODUZOF1?i3<>9Wnl+u3S3ofPf*S^+b zF}Vv%V@fsq)W-Ugk0x^ zP>8^QGD0N>N7egA_I|{%8!Q{ef)<=Wgx>5Zn0c_L!Km5Cb?fh3=5@T9giE zd~7trB@FSewF;DjYdM$X=UPSH5Qyu`DblT`(ad)!<~33r*^@fOQZ)0d*wKmxL@z0J z%aI?N1c;{R4jlrICN(>SKF9|qBWcy@A36tQ9xMD4D6)L5Wv^Iu>?%I9YY$$9TD8$D zXSON7bo^E?(qaTxFVu1jS1;Id6l)6Ja+If!d>o5@)#@mge6H6X!xE)xBUsE@UUuo| ztyY}H$gNhO<*2PzsO5;w6}sg}&y>-cX+hfIN=K&McYKY8k=sU)5an}r8dWR|ShCESi$)licMU>_ab|gQ{-gV+cUFd!N8^K-ts+ms^DxIP z_czqOd7@s1`MNy&78-mDnK#f_7mAj8S*X#h_##9D?Zy_ufuA7q5WhquWamhkG|0FYBWaqFiAhtbZiot<3$~JC9TDmbixcCR z28qtID>h|ebVvhJ{MZ}gq>*omb5@SSZK}=iD}xsS@rK|ZXz4{8 z68Z&cR0MCBwMYi;O$LasKerc_KeXTnQ7RsBMYIvwQV1fUa}Gss(E@-QG1j^irE2PtT3MS)r-K_Dwonof8j9jVH)M3Brz zJXu4vk^r-ep?xmMcVf*ZA~=p`_t*&$D?Ta1VrOziq%D-B0gE+|Cyy@#CQijC7OaGD zhf{5bT(R~XO(UdT)BDHF%}IR5{j>NuasnL76_VQ;YH zS{U`l^-^{Nn>s8n#Dz}ej!_Y?AZZL|p8f(No6;jHg1b%<0$*jJsnO;JsWCU%QT}TJ zl#R2&T=s)FAaJy8Cj3$2b{{5&X5VKeu3nHGG1HDQ(_wZKdrMsPp>n2WS5BoayPn!a ziUL!6VO7W$EvyO$54o@^Tvd|N3Y(2eX@xK1HLVboM*!L9dT1M%VoMzX5?>||@`uEy zW}esBsUHWoU2*`{UuJBwWqJbh#-`gf*YRcnD3Tvnli(tQV?FUESt4*aiIJR+CszVu z>($Jg{F9ddJ?Novg1EYITHmG2W2^irn~fe^~#`<%MX^GQBTB62T`T2!S{?h*5nA1&Indy@(1 zt!S8_0-#-95)p!`38|3^2MEz^r2?xt04g1dg^B!BFPN-z+~fTZ-{g~x#7-MRju3ND zvovWp-DcU)e^y{5 z{lhNs*eyy2!Q_hGE0uB@W%A)ZFCg9p(mx2&3h8}1aS zcdib;f$vO^KJU!E<#>uxzs8yyhL(#VvPJgXTllDeMKB@rWjjV|arl+Nrv2Ma+X0UZ zZ1s!@OK^VRK)~ttR{Lml1@T6LE%1-PHqjsZBP8^bbgPZl@ecl%u)o#5{zuZm|FQ?u zNeBL6KhwKKCoAZhL1*eBF>b^C zK<5Hpz=jTV2{37lhDU#KPmorzNCWqn2h}`udc$7-!wq2K zEjaZ0HiT#oaQ$ryZ2*4+cpO;fHOso>`-V%&GLS_c?mc1 zsJTFU+$Us}wk26bBPdQK-&)jgxQu6aw0FR=lVmL7$0~0k0Sg9UKZbt1a6p{QKq;o7 zxv=JQNHP%11J`_M#o+uUr=3az0q*Y6#VGKxrpkB~ZuMbSlv|AsR;*u*X>QAFjF_~6?J2<(D$N{ z`diDk=TG@ZyZ)2NL_9ij2d)^A8+o;~f|B)rzx!@aUjO&rym|Ai|8L~8ne`t;ITy{w z09l!^&qihYu1f_uxkrel{5D+T+I9+)vv5B=o3leRni(mJ+5?slT&0&}^~P624!<`s zf>wn@5bii*E<_W`z73ll0BhbJM;;Q$|}iq3r*Ni!?B`u@uLtGDU&V@$}cLicU`LD zlHsFQQ3a!1CTH#Ja{mIa(Z$#~c!R%&^U>-C?WrL=>J!#$9XOq7%dQ2nmn_}r=1{Pl zQqANgwN8tADe5Z13|O?<>9jMAqT3c4MoHkV;z{E9sIt;amqf{>_z>_C9%9$Szz5xu zYeFwQnUGlln+M4xQ1mY_xDqUUvsSIE(T>n)NRl=tri( zOGfXrPGX~z=u*P%#%O_xJ9MZZAiZ2;!zH`bI!0<&leQ?ph6RxVCXoF zR6(NBHYs!@Y832aa!;F8DVeu2Dv&>_!dms_E(1X?6b*#{@DjzsCKdw^@?28Hzh;N1 zNUuhNDY0((+%2uA5_VS*63y06p;KN(O(n;g3NjE&Du=SL$|+C65S3xvuw_C!b-7=X zly4}YA`2rghZim7ENypaP*1EJsUUvX^w?sw42|C1%kv&UI0G$j!Xs$?&NbY<^U|sE z+zrNRk!o#A+sI2MshkC#G~GIw%34i>xl-!?FX=bcKSX*;Z9Ag(RL<|5W zvzzG47y1P8!Yl?DIWV8$h4WLu!&v~}1E2}g;2Ai^ix{^ZKW1Q*;s@@xfJ-`Dw|lvL zgIeF|sJP~6(@ufX9hbz>+4)b$kCc+&gGgQ>l2_1*-?v_FLlfZ3d@q59+xIUUgYUj`yRyXP;rHJud7o3_)5;w5hZaw#YkTEazS+sd3XW}AiA z!my~23(%!eawR=AUtPh9^}V=Fl*b;ka>-7g?ryt0OL{kMGmr}HEC7@hr-gBQ$ej&p z)FO)j+{{#)zlu3jqIBc@?Qf2I#t6$m2o7pFuWN2czph8l;=ZJ$z`+}Pwo1xLU&PsE*RCk&(tQq3C%M- z|6+Z+x5T%0z*ZiopCmJR!(&vk!CQ?_xLqi;02(4?1NC9YXT}d-h2h$x$OYzGKX@yR zS=*s1_t=9B)27clq)O<t!6;cAgI>&4>BnVto;CJ-6z{7jpj@lT`d+ckO=xUY-VtBt^U&3 zmPh8ywD;N9*IC;bbh*qcVh-R3q|-odeX)AlGLe3(?x@H<|NZghS?~1t>S*x((+7nK z8k_=e^NEi+0jzRb7&M}#?A`s~dNq-cFmk;sy3k{#v1~`ggKE^!S}^}VB+>jZH?GOf z=g+}C1jE#fdVWS^GH_&eTJAKHMKRJU+3>jy?&YH(xlqrahx|wxF{dd<(;iwy^4m1U zBKWeARxr#PVUSVHPaSIs2?kW&s33Wb(?MJ6Mo0c{WWYn))f?|mf0&ooyL@|;HejD2OuAQbqO>v9CVF=WAEYTP4KO! zVC?ehM?|Z#g{_jYUeL zbahRVKnk^tibtJ{A>j~?UkuTE9jI)Y4>b9mvoeZsXL@a7s1WOwxJ2ClDD>Y3lnja7 zgU+!m>VYCkvakWDh-LIwZ$7gs#=@rnHWz_SVKlBr3j$)a3K>+}KZ_+nf^6{ihXeJZ z=2vJ}YH%{iH9kAKxs9J4g&7R9&|Tuiy3(yz_4!KXhb)FtuK$}i4OMqhXp1{fRTax6 z`v`$s@WJN>@0>yw<7r;U%9y8AhlsxHb@ueyHqhAqD{yJCmDGrbN^-Y>K~uKPe3*xLv^8i3NbXUU#WGd0J;VxG(S`K4c-2e$(cb^U?dF1Wegv3*X{exZC-O^mFDxLH4bpAcf*k(W4`M;d8 zoBe4M0-y~4<6Z9l=kEUQ+voYeiO**4e-h zkGFijHRT4{$qH$ph#VgUty`b7W(v*mYU;H5jMgtPYkfYfgJel(-6)(a0>2LrvUH1Z zCX5i6g)mKjt$|A;3W{H!?4508!}O@pya~LDd;u2e3>;|%eMh2wscn9ySIna5b`^`q zM|c4cJF|5Qr@}2-8u~`B>-)Zd#mVw-M#L?v;Mb<&t~*L(M-)^ZLwo%^|>KX znzAy?NnpEhF=><7Z8(e@UoYgCT@A*D$N>$P23j*Hs^S8)lZPC7Icp4;p*ZN8Ju*WR zs0_2(_$30s0^jsO$gRgJ=8iMn2!x{=@7N#17PF!;Kklv|GVG^Cm1s1d{?Aw@R5%O-E03 zBU!R|(%L?;#nXV{$;Xe_(}&C?0Q`nNh3kKu1US^OHw*tkd*8cT8gN#E?FmvQ0AO0@ZxkC959^?TU@oCM# zfAmu*|GVA&H+!o5-|HUiKg<73d^RBeYhk=qi3cnW(_C0!dVWZgj7*1!l_0y;mii4_ zco~;Z*QA@8l##FcxrM0?9%LUBOlXZqVB`t0Rf>V|==nYvH|zB&zN!FU-Z6m%P4>`%%knUHP` zKx+X`*>bWKLxk5Bj_`RD7~a}S3-0N~x2j9RQWY65?eYx1E@7Jca$e0aEJX)b?*-K~ zxUy2l(_nm?uI3%d(nU5;;rKQyo_G>DFWZ-NB(>jra06*Mby|jF1b{+AJ$REubC~>S z7QvVUs4glE{^g`oZ}05^qF;E$pkih9jjgM6KvTMn)Ltp=;hMoJ$VOIK53tSXok)Cm z&XL4sUb>`FG;pOC(HFqV#rJjgkSdRK<-P7(@X>AQkU=asEE(BNSvidu_huQQy~t98 zd!Mfr_HZ9Y{s<*)=w^$qWW(xzU}T-I_!R%WY~jzRmIS<1p_-`w_MrAm`KkqFPcuI- zfW3xF4cK}E@7@id#w`U*b@rqgu6g;EEK7*roE1KeVCog}N3+284W&&@w{C@*Op9YT zefiRkBhBDsEjtB5x~aP>PJS#fmSNMJ3q?L8_lw96tXYS|WxR_862{vYWt{jVTBN=O z$4I7RTcjsRO*OQgDp2rID;bAk(i&UEAJ&tD4>4%QnX-&5)y(jNmKPQabPg@)~F@=8)57kq$s7C+c}mX>MQLkR#1Q~4=K`C{T1p% zIlJ^&OIyyEQ{Sw6I!NZ>0uP`Mi8l_00jd+kxrroX*TQ64{wxc&?yE-=7s$v^o%sh! z##yjL>iVs=6@~Y5rsHAXm;t@ZwI={vi>G(s3C}mi;&~l`v=~C~rC@&L$pEfhEWw!2 z*%r6sthvCLo1z`mILLVnr_(vP+%1q&^P7VCltc$wv&!PK%zQk@QGD;heF1$K9)l{K zr%^pFkD&}PQjZqk(J-az0m0-lSBHO5i+Fg@7d^$w*g7~Oj2|stEm;s+$AhKfE}R8` z+s;wZAlS=t8m7+Jlc3hkZcmR6@~womwJ?f`0xp$=Thav}8plp&IgdMdQg;3*2YnF^ zM#+0M^K>5h&cW_(!Q@4f18(Q&z%84B`9j<)Z#ljfdX>w}icY*QSQ3RdfDsZDfoAdc z(H11$rL`-ssEK7G3>5&IS~_$h8@qVw zAL73sQ0t9XOULz{7W$uiv-erM2NG(-1%$pn`S5ZXX-&x%HTw0}Lmf-`y~0iU0j}$y z4C~fG7b0SQizKw;##PGhfFvqA`M?<@|K9#7J?^MP3yv(z?2{$%9a#dOApbb1UWMWu zcMeh^MnAgfyZ4oSr?tUB3u@HJg^Vt~6%X3cO-o=|df84FUYt%NB-zBrGeqg%Zx8_= z&SU=Xx0MKv^A(4WbQ3y5-HTHBU_Iw`g>Os(*v{(07lK^R=^&;VhAR2)-8-k_2X|;W zQ3FNVk`uSFc|z-`!rx@gu^3j>Yf-e)Y;vg8;M^>rYwuBvWpA*72b6Vx6_6 zpO@ug59y@69R~%H{;60143)KaEm=E=d$DVYId*$}*OKCB?pae+F+(&E#J!4X`1fXz zRGPsLy~zT#e>gske8c>2B%r-QsBScSZ4Z*7n&(-iu;F&c5$T(3bi5I1pv7@H4YMkh zy-q_vOwdWuS}O*gEbh-8y(rR$6Fa}SygK`I`raby8J7&XaIg0Cemp<9I*y8d^~3SWz}TGZlBr4GVWCI9E4Xy338SfP2;H{T$hMB-H3P>P9Z0`z z9MPg?>7AUM|8zAt9bW#muw@RZ>}rB0?~1};jN+uQlLn7{80W)e{=P=>T46%Jm5_Vt z((Wk_m^H;-)rgs<4)JMxdtFE58XR5VNX( zetLR*Sv;%kXxmO9v~!d=pnW{5M1HZFk1`IZAW18VxEyQ!lTV||!NpZ?IJ`nT@Mv%yi|c0 zkp#u{I_=>({Sp1Pt`4J2e}6g`cB~p7{%-fFO5+sOdVF@ro@wa2={BVfESB)JY$GwBq<54SE;- zk5|3ROS8n%_hTA>HQ>ryE)(609(o>LuPV$F!U@`^UKU8tLSAmd8Ct(|uYMqJ)-vh5 za8w~=USo~os(6VN-An9NSPO>~fjh(&mUu`}A*<9?|9JRua8a9C5Q%W^535)|jHlqS zVK2Rao&EqROohgYV(&W5JTXxE^Wd-R5Hj!FmF@s{n|=>WH4Du~(9{gQ+Pw7)&0z1* zG)`t!*}XWa&F{s@+D2D62=&8dsa!e#fE9nM9he6twr9}<)cqnGoqPzYEe82({VY`oE?(pJ_#4Wkf=d+Y@QsS4Q!Px9hJ3qlDxpn za31_4TG+PjcyM-kb$WbubvhW0dLIV1lVUszgT?YRT$~1J>P>cQH7E0e*7cJ3^!V{YCHM*n)H>7DrPr34Ao%XOZWhgx86egk~NLwh|0qHCWvJ z=p{LeVH4g?Qu&r%PsI*qnp>qJk^X7nYl422bEwKIiNvMt>Z(NT6<%)YZ@FDins8Tel|N;G$W z$r#h!|0QqSaqi(7XYr&=U=9Mtgef2&-8=3l7_u~8sNh~_a4^Cbr~GS}7vGyzV}T6^ z-^DCMODWuNIB7bAhXvH6-4mq04era6^F2@|4O)&nMg94%y^H?WRc_o`&Sqs~KS&>} z)t=CS58kFN@oH+5ZE7XT3q|8eco!^?cBGMwGx1fE!cnH#fOM2N)r-dlDx;T=Gr2O) z0`gfvJ`2d-kbt~EO&%H~WFoz5Qba0L8<3GQuX-WbM1Ay9GEwUD$iyT7%;;sO$^A>L zPM>fg12%ULEJvR0aC}CPTl2zOwqoA|cg;*@h15Ei%D(9hcAKJ2 zSCpg0jftce#~9%MA?Oy=6v*moYqnpRB88Z%S``8d|5LB`DmnKHbA zaSwK$ClOiaP*17JJXj{-SSv4B)6*nE!KHXBtkmkbD^kv?_$w^)Bg9`RW9@f{zw&6H zNCd&gy9sMvr{tHW6K5pei4L)(NN(th&xt&j>Ok#ZRzY%)l!&E@o=f!^q93wSA)cD8 zFiX6}Bye8B41evsWJBced+WLqK0ySKqurO;Fk3|MoDk;O+i<5}=e1^UMlqtWz@^)( z(fOx~{^07<#j&z9WDXtd>Lj;D9)9O@W#)gU>1>XM(f59sLQU}PNi&iu4GV_7_rch1 z1YTEWY6BzS+niJCsXx|1@Hh^Kp0d%?a%&|S*7d8t$fxG4tEzR=z-O()-+boE zhrhWZ9{ljvK(V5gqui9m{aZsn7<&(ZcFYT|hjUD(vQtR=Rcv?gL$u1K~?uCT$hAHzFBm&B@E!{ki@rzTF9a&2CiKtKWxZXZw}T^=tN? zmIuqOD?3592%At}bRe1|lk~7Zwl=Ptc0DMZwKcClNQKwaAvoir>#ur-XRQ*Ks>5Xy zidmk&OIciFc182H9vGtgL#z5pj1YhY50%e zB!n%dvZ>%NBnzQM#xgBe)73UXJ+*0=dN=dJxvtr2`aSS8OHx@}zTDPhcvQUL7#V$; zH3Z+vM0sE2l8}&V)=?2D@;OGCzlPW@7Xdr~fJuC$EwgHqgEZ`yX|Om7X6Th)T&E)1 zLLscS@MtvlX6qJ9$`E>vjpw^8-z93)8HjFDCD-xd@JApJEe@T#E^YnW5KgFvI>K9Q zA}>9-YmyDQHM-iu_r_bG@#hgZ1|g^LxEVKNRGnjYW?i^uQ?YH^wo|cLv2CYf+qP}n zwylb7n@{IGJn~=sZ?v9#? z8h@{*8$=FU#hg%I-v%E>?@4r7^rioVJ_}QH;~4E^uvuKXxVj=dzhrN1ZC!V^>b$}b zJiq9_>{kCT1@G|=&AGX{c6|ZtUa~69vhF>H()9JHcBoE)r&wE^AFb-N4GpDm)+~T=0et`o&ZDqQyXHztGsJAPR$dTl5Q;L}gepS( z2z|k%{+`ZF*q+Hq2g%4QL_4l{oaZI~S4&LnMb zUD*I@Ccy$yn}KeFQ5Q!6W9O3Y#=)_te!x?eQaDY7;I-(}>EX?)Ut&3u9v56@hLKlX zGUYn=*i)BKBEwmC?OQs(zp>JYa2`_=@EYtqZGUdJX)-FT!xs~}lFW&ZzS^e@p7D){ z4Q9enYm{4TvKV4UZ_Gksa5$I524^33f}dd+iO5DaQ^-qOR86N32S4@}kb-buk&Y+K z^G1#i>^M9H6>m{SMnAqD16uBqs4tm?*(@5lI3|?=GthqC)tKo9BlUYI2+_N0 z{bL}$n1+-_sEJxNO^q8LT>=k9db%Z$Oc;XzN*}!#77k7}|8f7WEZvUbV60SAJMdjm zwMuC+$HG289lW6upw3Tvpa}%=Vm>9rSGatnTRo(B<{`CMVAkE%B|Kti}e6D3s;l zBis77u__)XH}h<^tV*yS#odX2(*7(swVC#-ci-{0I;1$#}legyP%jI#EF&Y))mh0jK8hbS-U#Wk+JgCev+aIXnf=yR&D8l82 zf&VXx)&>h%z{>(p@*Z9q`qqEq(h!f5?1mj{lQfQtJJ8^ZsG6mRnsJw@>=OU;cPUWHSKc4CL zzY(^EPKd*rm2I>TB2LNsKMV_#xts3~#pe?9XCJZpan?-&T~UemFKCF(EOo-{x~FmG z+!Pw*fS04x)+ipx&fC6_OW9k*u{_y=P;~U~kU+8=z_Jw5lOt_{!JKgaPPb2gsh!h{ zs{Q;YDQsWhAKJz%#4Z$$x28I{TFlWI-0RPgvv7|zsThzES0r*J9cdLfpL6SmPPaF3 z=^e{7YX|0IRQcTq)tUg+YX2n;<`tf>k=E9>q6#$e>AQW2|KBK?qjUBGK+d084=Dc= z4qw&9!>NckGqjojpRch!s(Wehw*gVry?mFJBAH+oU%UlhTz)3u+0B@LqHx`g-MF7x z55k>_Y9c%_n>br^l9nL7%wjanfO!5Hh` zU-L4>%GSr9Ag{6ZMA0W=9XqgGC7oNzh5E5GCJh^Bw&`dxH?)B0AcfZSd@CZjqhD6z zDa?H^&y*fy{bcjp2-)bD;hH5q%dHG}s%C|YQ6%l~;Eg3JvYbH2HhwOl40m2g^9&bv zLmN;LXpeZjoK#34LpgYEGEiEBV;qjEwWF)nrNkll1?3ZrMweNQtcD5HzzB5FSn1UJ zuN;Q{fR@~WuU_DH=9hgn@dsqQ(TE7md*dmA!lK zDf;i3*1T|@4~(_>bw;O4+mA7EIfe!K5HPxV7_#;TRkHNF3dAGkOpT0-J@vVVeve&w ze>L5@ecSLU$4Q06Kyfvu3#X6ta_0H04&59ujg`ueVfR|V71)23x(K_<;)X3e0e)`* zuC99c+;jmPck7q$we_ZIFNjv)o;R&+$&*{5bpz1~n^qiwIEo^VlKWy|J_e$xeajIz z4E~wfWy{hR@0r(R=@YhwR@V%ev6&+Bbki1zr?thX9};N6qsP@x_L~07@4Y#1txMk@ zP_ zUjj;z`G=pj5JB|U1$VWn=JcSV>V{%}1jUyvmJc@kt&CSJ3EXoah9t4FD*Pd`KuNyS zq|8{k-j5Jfl&4i99RBH|fta(Mqg!Zo|AeWJ!Tg#=`IKrj9lX(|)Yy+)K zd-6xGXLJ%E6_#@;Ckdq@>xHjlj33^G+$yv!O z@kF%%zz*=Gd;+*X(1!xvOFk|Eo$<3q%YYnuA(24M-GfKwg)@Emfr6iIWTc7RFSzopw7tle9u2CrqsW;pw)FOjm4(dLaLg_FTn+CCeSxem)K%&6_BSV$_|<{ zXkGSO1*aBy3HrcOmXN2oOWROsBpQSG%-_`Qoe9))z?&bkTPcno!uegtz!N6pRYi*R zq`^W{-KkH>8`QjPj~ElItb!j;HY9a;Bs1W?=vlm61Jbb|CfuM{dSuX1Z1QT0Ht;ai zUa(GzMZnHGU;yclsww=qF_l9s+L1QpV{};SweE|JJNW8s%Ri3i8z6p zOy9jy1p0y`qsRs2`8uh3i80mEXdFF`6QYH6QMkXXVO%tJc#w`rgHPbkn#UZSw1XZ) zmsOJ?ewTZBf6WltGGQYqB-#5GlbRZiB2K|SIEwL=wGzf>HRkQ7I8v#u?a!}#^`{U5 z{%CNF3F+d(?qx?Q6>adPC>5<>XfOL%qV?a278BEbNX=ZoSfu81Du0QmyNPE!&q*&b zDDDINR%GG4(5&E>@CUceAAhF6d49sj&gYi=bXa({bh?1vsd-P`oF zzqs0Faha|(X)k{3zy23(;Tsf?V02jzy$yrZ1Lf#=VW*c=2sn&L0&If6h?EHMTtf|IWEA=02IUp2lG`Y;Tq-_pPg@NNIYrOA4 zsT%G>=Dm9P4fMPC?=TuP+BA%MZ&&cnX@%G%YmGTq*88kOHE^`GDMI%W zs>0?JX8x_X!6xQ=c{);dsVQGew5+=@c&QUF4)l%Mm3htRjPS3^5pZ2CJ|hu(EHMrhe<> zx>6T--{_UUl)*2cn?T5>S=amA6b8!<4F{bweMr??mCM4oHF6 z+^ng?=n++d6Zb~Fuj=%CB70LbmFdKqMWICRg~ZX#D}Sjq>S$RaZF5Rm=4;*RXC7E~ zgpAy#a40)jLrS2K&zD~(?f%GM{{yMp9#ZYfD`bnEOtJj@qy$ZrP42-!AI_rGoy^S( zi|Ia+H;8aua$QEeWM^nShCllZU>OJc|RG$DlJDxFs?)X{LSzi>^tspJE!?tJO z=$04bCYAfcfnb%ty9w}CzBuvTISCNxkm#1bC*OO-#PfAav9Ym0E00*EC}jjcp5^VI z8-iQBd=W;B6K}Zx;RF+QthPzEZTq?7!UXGnL*(p8TJ#AOoz=EOqD}y=ZC!gS_XYmG z{JeSo=i?C%=x11m5@yZ`k!EF5gUX;{4MOfWvYYscw(KE_xBQY&4_na~kA^gz0V`i5 zfj~m%l+CUCK#XQc?)5e4IU!s2BGC^Oi;zFGW##!a`!!2C7^cjXQGj4Y_GC(QVYZ&4 zoW=PMbGCwZu?I?UGSBlDqQ`g7&q4WGS&x?HxZMp3~Eysjt_JKmHRtAl<~ z^Bf5^^#20yeThRQqfMp5*!G>G2@jw!DII)$WLI^5*W+U8R7gDSGtoydg=L4RcD^D8 zlHk9!A1q!g{2G!F&p3Q}aYYs*<8^uiw2$3ipNe=sYf0}0bZVR%RQNw+yM!OJ3g z{+5Eq1{WBc%F#*Jt|X%C65!lvQtwsFK!Wpy4B(~Vs7R2W@Zvv5I^~15vx@;cY6V#% zdMlC=((jKk2jLGZ(z-Qd*QisR z_k3#b1J@&->(d(|;L9(=zDtw4=B8;5u$kFCdwi9oP@G_;@ops-)Kl&j+41#$ksseq zTzex{QPla(+$-Af&*5MKl$$VdLe9Y+oNB}!kd*UJnF4Z@;W#4BDyZQSoN z?UI8T!I+^_aSZR5o`#zy-MaR&KO7j$9j__)lnNYl{&r_3JUVU;+|hBEs06c!#lF3Q zpkADo@4Y*!UYs*(4CzHNnRDrvx=GL|R_l2*pEwfG8{u}Ibd05yX(jj-*PX66SwD*U z{9<$6MD4xqvF68Py9Q+q*t+v(O3lok>AXrsh(_4j?q;Tx?*5upTQ?HF>vFki3kSyp zEy40_tr*WFxnA6O=Jql7hqS0Tha(zrx)x0I|xX;@3C7;<*aSp#6Cj^Pq&Se(;h8_;)}NP4@PJg0bNE&Eni z(O>64b{*O#bC#C9dQ43XG;kxDD_NOB>MU2*(bgB7E5cRDzpB`PYFqf*H2slvmT0>Z z6CZMGz)J@wd(m5_h>{Iu=MHu9f6-2X$AG!*qTr0!Gu*!LMm#*#GKT%X{5$9A0Aohf zWxchHj{$rz@xbTi{`K?n{PA;)5n1zWs`F2fiGgBpf&zB`K=YyeW6oV*gn0bq^z^U% z6xnG|9nqWxV@CGyBf?oW3BcjvsiVEaG7fz0obAYFe)_~Tm$@esfSv~oCoKD!kq)lI zO01N>r7}EgTFjiUZWZIF8lueF_oqm$TL4SDoT}M#*t6|+PE&-gdT-ITL@P}S>>5!^ zZ7{MQp{zR&^&RQ!i~vCZp5BrLhoT=&JvX6~@a(5%fHv$oQ&JzI<2c=I$pCJ7|hU;Vi~*O!E)RqvgX23u|C^wRhH3MT4hxvQXG~1J$|?Q zlec}uq`C;4FMBc2=b;*DWK^e>9~`!bE7>M3V@44t@Wcot!Q7=d=_X~mG*J8n=7SHnl*(OqnDOgM=^6W-7(WMybs2CS2Eujt3o2jIY|twiGQndy_DZ6|jN#PF+tS2!6NB z77?`z@{J{_j#TN%#9MlG!Ye5fdb9n?k)vXnHY$IryQfas$Kq~WWl_yLd);~BPE2B)$NO7Bu3G<>VuDgAyE>Oh1pH7; zDBaYWqW?=VQ3HyhVT3V6w4)}az(kxjn#u0a6Z_nuSU#H{65!$oZ>le(UO|z5!#n7j z4q7AiI|-$k*#&a6&%_SV5zuUQ5f2KUL{|MKE%Z3n2QciN?cv>yjd$-xmQHqUgN z^JBNph1zyU#1JfiQr;W3u%rC@!I$@zb3gb}Sh%0*<$v+z1Ci?wzGNS3mn-0KTA*XV z%j7(y&ua_Wicju%E40Bo(sUU);=~s{*zmeM#6&4{d*@!eOLqRYkSd`p1cOTVBtv z)_jEZTIDR4IIH~@My@s#m^*_>soLb2l-Xds+WccL-WDc$xLvp~xQ`pp4o$8#i>fsa zJJD@uP$)-KmOnDT9?q!#&JTFf6$p^Lf_F;0S9RNQymC1s|9OD*JV*GH3cG| zJ&lmPsE<$f;$p`8KWGuuMogpk6~=PCHq^J|%GO6*h;r)2CR zHw|929aPKM+Tu~>KC8vqf(3~P&Oxndya|J6+#UGMd@0kSqvW~V4rO%vq1cdG!RwV} z{1Hv&xQ;|-ilBSVl_?0Gy7{Vxyvj=mMnyKI`i9DXWgXxDY)@$TxC=VmrP&c*SN(<) z+l9PA9NJu8gs`;px890Rx=JjXUStTX>F^ZCRv3Vvo%}+ZbF{TIrl}3rz7!#Xe;sTx z)kGFl+)1$w_xY_=%@=~{kwxKe%Vj2K#T;4Ft&x)OxpE3EgaD%lxb6H5!*{R-JO7R2 zy3onQ@1UDIV(aSqqVK!2zO{ALj<4PoZ$ zy>OI^hKuoOfZ=wDUMyW;uJNc_p0r4ri16YMHTQO)DiCp#XwR&}eUmu$FyG{Lh08Ev zQ_7Lag>g&lOb^V7g7EpgV(#{c@j{9V5y@&FYhan5ZSE%`XSa6Yi`(XTwN)+PGMDwQ zAc6Lkj0J}Dz@klr**F;#(J+-{Q-33=;1SU|R4j8FStS!1Jes!z?o-0Zt}5wwFp|&4 zv4vwBtLVR9?hxV%Fgwzp*111R+?g8A-!)#G1{R@>v!oRmqgNZt&T+JHG3hOXWrKxi z1cr?!B~dW+3_&57={k1z1>f*o$p}(jRQ!COJ^C8y5KwMGk{dj6r!%nQ7M>HP$D*A+ z8~GdG+|cD+zq~E-9j;GO5K3bF1)B)jX{rfh=4frxDVZ(YOIDyVl5V=wL(w}xjZ!p^ zxP>$DaJ5^_S^NzcG~5~|K@H_%*{c-V1Sl8e*tWK{Rt`W#hp=uW;tAQY}3&a0rHm5gGuoNLC0j>HeSv z$+YYWa;#x76bpfQDE$QwGceGU7&S5INZ(XkH3`EdIOiy$%ho1MW!kS z4>QX*KbdEAAzDTKsupFg3JuwhZ4SQV%ZZ8zV{&?=E`^W|FiG$Y$enl) zpb$>WgPiWP%Y!cC0@&FIY}CyHaB#MwqeCA6I=rq<-?=9l8gpAXg09rzP9%5(6_p~0 zhd9G#eI*NwM7GNgDli3&8w-q&iTbEN|19w_EUGG#ZC~^%QdP(D`my5DMsH+@F~>)r z(h4cOGdpRteRsotyjt=~HD>Awy7^s!8GESaSW>jpSyb<2XGq6xEcdlS7ta$JDKh!& z_GGraGxQ&miHuT&O096yP%%uxF>Uo-`4AfHI%095?xzThJdy?K1guh5Qkm1-tY8el z!n!GT8B<<8F4yQ)SeP0DaO6ZI#@ z`))Sf%s-zT=7F4YZOzi$A#H8yP9(F6YO^mq4opxcsI3PB#BCZV&=*Nu{7t#fM^-Zd z&%4`rT+}y!E~nRD3DPc&k$u>2zKt7Oe-8oID`GE#-@TD1dVAjpb`4}?0GiET8A_Gq zbJQ>|v}kgc%^aQKEPv%)9R)Zk+{hO8Y@RMFUC@3@|Eg-2 zWF7;OW9LIyhkC>vKzbm}YOolX37kaS5vPIy2g!S-omw+q0fEgK6m(B3TEbqL3W$s& z6MoOuXLNNbVyj#db;BB|!Vn^}e0YcYOdWu)sizfc#P*WBy0b;0oVpc>5h(3H1qR@N ze>u(b`(+jR3F#u=(qud+*3TI5%9NZ2xpY(btL3o2h(CPi3471we@pWo-4n>XsU+yi zv|&BSg7#^C)S7A4e7;zlod|w+MxNx1`>Lqf1rR>SfoMzdvI=Mg82fvzB9Q+Y_J|N_ zHPHEEje%fl7Ck-F!|8m^w{ejFvjfyXvSaGqySJSFj|}$?wwz##iqM8&lob(*Z+C(( ztKJIX2f12pP(M~JgZ1iZ>t$&l+&Jwm>`)w_lSJ+l=i|Yb#{q$(jYUU}gB*J30b}s;+vanZ{YhnAIB=4~ z*jdsiNd3%=s$k1Dgw-?c zUhpU#z+%)xAv^~E8fx9!o^w}?sy;t&hA^=p5=E1N*iHb~&nceYJ8K%g&-VY_e6}+H zxBz#3`a{IBn`9g>G4L14F6@7)@uCs(XJYzDp;@9?*`fzgytqdM8O-|PhXB4o0BhJf=l0<<942po#YVB-DPBF>^PDg zqRr_%xo#e+`HKj3>hWEZq~b=X=kIB2dvv**$Pw3Z@@|*oe1?^{7nscwDOfkBN4}W- z?ny%I`7drZ!=CamDkXI_V14(SSkI{oia2N=Tv;R|<&XTS9PsE_S|UYe!qm zw>Rkx{2xlJPbvixZ{G}XDR%e}MLqyM*Bf7s)mDOMK&KmjFJLwxhHttGBo#yBe%5WD z<~iH~m1n5FrYEzGNG01R9!XdxvX45}+jX~i=KYBM(KgM09|(*s`TX+`W>Zb-CRJvz zE8v1`k$2YOnST;kFT+9i>)Y4p$*%g_)yjKg8DMK8um;!}e|UQVY<4%InZB1S2s(k? z^%i*DlHnB`ZK9D5ANVIm{-Ihs{F|#&&p-KR3?P~jLssr<^(v6{)Ui?X?fS7k__dSG z>)Hn($OVbkP|BEA3$9MB=^)l_NjYY(izqPm%&F{{<1NyD|UOL$zRqi+?ZhzVzgPSNaDObk9)!cU}+%Kfvg&(d` zh zD!myE5wX0FZ8=+KOr;Pph&+lENdV7bHIkb0UY#rk+O81&qm2ucRy1zH3JGy@=#VXU zVN;=}0F~GsuLp?#gFr`)K~_9kD9A{aE&d=92-eKKsE|7!iZ9V5aHLxO&f2a?cYP`Yy95KRNjvb11K0EWL6FJnxigoAFn_Py1&W=iy`)ES4KE$jQtX|GGS?Eq;38tY= zXe?e7mZ)?9KT@wQH%z0vu5(}1l76DP@g{c==Ukeb(FyHgL-O{5R=L!S$?p@MLb>C! zT54z{I>aeBoN};Dj{sD&5n|ha>l*k9%_dyA?{(KtPq|ny_ApLBVxQs+A~cXZ!?AIHpVwN0_lp?kd4 zGkJ{G&Tn7L)6u__KZg&afh2%C#tK6e;OR(SSw2QlIEwR^88OhqTQsj4i6z z3|)AjUpnmD8}p2ZJiB4kqar!K)U`jKQyUfT_PuzkPD9bUi_mQFjj`#bJT!i# zh9D0-eE#;eBD7Vezpafb{z9X-G@`{;a57MP`8shn_i#J*fWGmC-^uCWzAGi5Vc^XW zSbh<}xtQ&!`2x%|K23*y<9c5~FWMbCb_ z$H{qHOZa-foWCCKUgc~29)njmy*Vu_8`?HDM78XW0>9)=uaD$xABZ!y7z2LQ3v;mY zHvmrUJS=9xy1vBep<&{D0V_Pb6j*DuFMvL9lCfv%OWG`Ro=(_%gvOaLO&mYXc$y8Q zG#zC8oo(GRVtNPJNGBW&B|(k#R??U%2_TkPUAtIBsessYfr~y42b+&2l@;wS&%N z7ioxJ_WxbOHKJ{$49PXJT(WD<;WrUy9medo0k%^_Y~zjQ`HPUk93* z9A|@lKU+-k1jQBU*MQ|EIXC*J@Br#ofz6FjN5+X(83I(^K@0@Rc4mq+6t0_gH|N+T zmUIVxr!SND>uJw!nY_b8r+EG^bZ>3J*_$nl4X|5zrb(hh2)Sns`xVRr*Zw*C89GkY zW#bcG$S=^PIAY|o?ETlL8jV+^FziMl6sbApDgnHD7D`z%Uir|}6HO$udEfV;E=7M8 z4b(!vO$5E&bKc|TC1krRMR*Nh$c}tbDM8p$C>dWn7gIq~;1Gt)Uu@B9dhx?`tvd~i zH+uDjtqcHgHVuk&8L}hS1|MwMl60ab7a)@}HqrIxMkjEx19(W0lj;?Jq#_0C&>^Wf zK?Y1)n8o#=aF=1sI@GEkN<+syl!+3Pv2aHFj3mL*F-^B=$>6o9>Ef4o{#L?KRE$xS z{1Q%9s#+JU7U26!=utK---AZ+Ve<3hbLc{wMsr8?4b0pJ`n-l%4ZSD5 z{(AR}cnOzEY4M}`U@&m-#>IqnE)n2SjaBs+8`3bQ$Mns;B2E(9vo>Q6C!)QVZWuSq zidy^nUX_4$FJ37W zY9y(j@A7C5N)7jx4@Vq06Vbj@S(86)E)*x$f#&fM& zhy;A;Q}NCch|X0o3rGDoX@nqrmE*q@XEXCL6f};z>ct=zhJTue4T8ulsMdY7!@MB` z;g&JmIaxlgpXZ0A-Klo)@V?ytc(~jF&#y<3qcq1MW9v$VTcr`O(UX$@%8;L>Q-%oj zvU|+|a%y&rzsQ<(Tp(?{-XFu2iGc=Y&>z4n(eI_}_sZDo;vJhd^@2FnYL8wKJ@=U* zp+<=xXH8Z+!B|@-Y#SEqq!OIV)a=nvJHilXz|eMA_qL^am<^$h zT$nz|>tkeDmzf{jG#Po$Fu&6W{JJ3l(2|FqcZZBYBqNEY1?4}QObvCW$VVNyJP4ls z$me<23~R@vB~yYRkf@V!6v(g+l&ITd_oAIl*HOc5ges`~!AD7L4rcN)pdotmDoy9X zDvLn(#P(+oyi7^xQ9WFu%>h(jgFhG-WS<-%=Oa2Uj75V+d9#V{jO>$fG`o8?(a}sL z-t2{LPyI>fV~rY~{U`<-&{_oAV|v7U98>z4!bDRo z9%lFF2O$PF6L}UW*d>JM`4=tnb*2d<1Hia=BfU#uZ3``&{>9<*$};LB;K2 z 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 literal 0 HcmV?d00001 diff --git a/helm-chart/charts/redis-20.1.5.tgz b/helm-chart/charts/redis-20.1.5.tgz deleted file mode 100644 index b904b4d609a217af73341704939544cc309cb339..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103244 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

|5+q_! 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

feOlEr4s3{paiVq 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 diff --git a/helm-chart/charts/redis-20.11.2.tgz b/helm-chart/charts/redis-20.11.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..43ffa3295dc41ffae1f1fbd2f9d3a86ccb63549b GIT binary patch literal 108485 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ⅅEzqh!s__c|>2_$E|cl}giRNqmhl*L`() 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 literal 0 HcmV?d00001 diff --git a/helm-chart/charts/traefik-32.0.0.tgz b/helm-chart/charts/traefik-32.0.0.tgz deleted file mode 100644 index e154c1fade647f08a63e69f2966888a2a6617477..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246624 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

IBPsoN{LuFkdfljzZ6S8#rWVQ7 zY=#gO^QbD-AjcrT=gSqBrmYl^NldwXE(C{Rzd?`FvU5AQVTGD*#k^gqaQq7!tfGKg&A+d>L)QxriT=~qI<|pG9y1pL z^|LXp?Qvi1O=E$F-WQGbzalGD%_iE4HdRDc%G0P-;bm2d&p}EczQT_e`d;HkT*BSz za6;&7b*Gpzgk~aqL6WTg*=+h8oA8;4F`g1R2!DNs83|1l#)~ns#S? zro=8Rs6N&-C4-C*0h6ggU*vbi~;e0j6gMfu^E%eYMliCfw zEurd+n?4=A$Tz<2j(1M3U5zhB99~@8*f)Gwus?2yH_^FKxg^}Nx!16{*H)@WX4vXx zEwT+z55Wu_rwy-{M~UJenZT~^w+JO}D7o%gVAui=_m<+Jk#DHW52|>$18{qkz|N7U zW+0vb%o;6N&h=tV3k*evyxb;c#1W@^+78&=#sIic`>l4v-MGVn@|>QXC=bxuH3)eP z&BlTgFD~G(td?qeVI>jc*293~d7%`jKL$6cfuwF{fHJ=$x-z}86OTkn%MP1mX32KV zcox>z9frxPY1JvdJ&7v=)w2`stOr(#bsP7C4`Q^LFH9}C)@`U%pFL|s&^Nswx{;an zKfy&Tmw!z6^q_XB?}VAOf?7XADlEK6J=8brR3||v28ML}K8-!GIB`!XGB&phYh|GC z&_>`NQ*9?6nVd=;wzT((J!A!dHraP2FwYof6vXM zkQB$|Qwz(#fG5?mm`KieM+!!)!1|-0ekB2lwqYm)(t%JbZ~0GW?;8)?;p4Nt08=WN z>;|HYL!beoulG#%l_2mQ8$XYOEVi5@23Bf{DfPy(kvN9p;9B^U-q5wi!{>I~FKl7k zt@%aD6fl??@8}l?ayE)&TP7F)ZN5 zO_hd6{t9)-0*w2+QEZ$Hh3s59P&yg1O)#=?Xh)0|yC zgV1^c9lIG-sy2ESqy-lI$(?v54|~w~6Ka$_W)4(>WivVOOj(X5M7C18?@i%5iX8kW zt~QcNmL8{uwx!)X`7=`?mVJ%{ zgI+cVJM^-KG1u{xm$EQAAR)^Zb#&o(hf!E8eSZYNyjcSn)F)kvxBMSH;@QMc7k7l9 z(~!;~-t)BkL8cIoHe>(<9l4ap$NfDL+^ohkmQH08fE)}++5s8B901(wt-;Z1BpoyX zzKB*(A#@2C`>4PhR^isCW}P*>^c6-_JT>$#9hn4&gak%@+dqfgkKqAZka7_YNm<#s zpo_)2*r;A+xv2Y+M`_9q*@qDj|CSP9$Ox9SVK#IjL=c%U!s&`HK_}=kT$;JV#ANX?91HQ(Y4GGK+ranle-vUjN_Ts$vh%zSFYYipEwnSo!zBj z)I-g~QY|+;Y70G$Vp%18a?wWO_!4W8o&C81c;OXUo=w1vl@GN7!DbE+5fyXNaC3;W zcYi|zNgc(=AozRlG0=)zY>sP8*Jzyas>*M;t|~Jay2wo=0{HHmK#hBQ5MYerI%~jY zzxWmxeM~Pgwka#3o_|>~QC22C6hFWu%fR*3O76>PkLwUt1jgZm*3HG|i#pt>4udFY zU&(+2)DGx0eH{Q;OplSAmNZF!Sw5doU$MV9k)ouB(bL4l6mSl!uq#hrg=M@rq=E(= zrfKYEJy?JFx2dj?`hu$g62W=j4|tA-NQVj?7JQC6XKr1&WtZjJ6|862tate)rpv_> zHLDwj2zP9z=eKErCE1E9BR>aA^9O}%$01*gD0CNfmpdI zzAF|G7*~XMivpFdg<_ari`h68QO|b-g(~oIUqlD13B!3{6^AAhT;h9 zQ0GoEH~+ziiFTr`OWU9&nhov9{MvH8G2+CQD-%AP2r9nbJ!D!n(b-P)|Ag=R~QHrqc0VuY=_W3)Qf6diL?R=cp~?$gDjV-0NDZh9p&hwr13rE2czZG}vsz>ann`EZv-O-!bpv;}8Wi5t`H_Gn(s zho`RCi6~+>5Pz6rJqb!;>_0yZdzA)hA8Iht6M>;2!F+b zoZOFfuIu}w=7wgtR3W0~dg`Tdh{AZUY?TFtp9Y#^#c7%9{Sbu1wXwvzgk<-_S)>?> zV=l@ADLKK%(lv!_%6!_B#G?h+nVOzzCpSrDJQ^&5f z9dV;KNcAuTbeZLb{=!iuqRC(&qN`;laW0ZL@|$;c=$Zw@L10jbobs`<9U}Wa`4;6E zQedA{yEU63Is4`Qs^EIbMgoV0XQCP^b=dAQTPbY8___E3I^p$JR4fRgx&c+GcVGk* z&b(U}w`milPaey4nh{u!Q^Q?!Sn2Mm4p(z>&ZFV}48ueW=ywB8k$ueUR%FM3`dzA< z3q3e|4sH=dUY+S?0GwV#m6>BY28+sa0|6C^EOjz`?l+;z+t9cY+bx=`aZpno8(HB1}(-~flnxePIiLbT4d<2omUBp{fJmFXYxa{d6R49rlexBel zX2Jkw<#ib66bvuU%Pv^NvQbi!s$~3a2l?0ey9dQIW`mAcN)~CZue}Io9J_EV2P0{i zs+~d`iJSKx^&R$$kKigVM1^Md1a_eje|_DJas4|+l9e|Z6|43#$!s5mXQP!~tc*)UubZ%&XYSh77nxU5 zIG020UL3_5=bV;}1$FPKem$zEeNIXL!OMkG_RLoV=gh+MDu-0nEN-Ue)-X;Z>kPAt z&R>S+=p-kZXLXB7`-W2HnE&%yT*~~ZQ-q!4WEgn39XP{Y_v^RR*<|-sAsZ0N!8}Dy z6jPia!}?(23pTFG^bVE~9Xt^|+8`4v=G&dBST+8$oVKKH#f-O@WpdocTrddJ zIP0IBW@=DNY1w7$lHyYK2xWrpq=e2@B7y@}0O`FiITPlG%7qEHj#iPv-a%%GPGZUV z>67Vn^_Tpm9HE6e`%(PQT8)zrKBu{-sUvAn{ccowgW)#qVv}KwXWG~AX6=7Bdk5NE z7M^KAOTvl0yV_srb^Pl(Eeg?`QjJpWux#pER4$Kko^>-GEwVxoj3|zdutkMN5%hCU2gT#Zwd-X4A8L&zdqg$=^vD^YbVodQfk5xGNi(3=TpJr4Rnp1;}9U#Ki{x+aku z5o+pfJrhX0eJnSjZ8#-X%&C+`pqTGJ-zGeH`}ObW-M@_%B~8JaY3hm z<8y&q4{I2V!p%9`klCuQC0k5!eAW-^Ylml?+Dm^ic@xR?H1Yh_nO@nifG8JMdZ=7a zD(~wIddY@q5W_=>Nazb-#~e~DRYc();+s9k*Sn3bbQ)af>yy+wQ$H&^QS#K?fKdYn)^;Y;g$GKF&*^20-JuYP zasK)RV#q|Z1n*?`Xfa?UZlVi5#1$V|)nF__C{m2X(Ljt0jR4u!7?cvBFzO7P1CF+x z?gGcq4-=>#f8JA&r2uQqr~b>*lPGp<$imtQNkdIy0v|lvJQ_dZ{f5TT0I(Rd1AKmu zA0FOSWwF{8{Y`mRlcEuCyG_-k7AuRz_jtM8Mr8Mg?@@Cr&PU4g!@?Hb#=VBS2=#BK zG!iklNQg)loK)ogiuOO5C}k;_|Bl|qI&PN6Z>J|S;WIpLz1gX!=GDV+02&q!K+oU{ zBH6&2m+}Spws(?PNzw|+rCYbLtx%m(O`=8G8+vox|Ft11sN=dWDn3P-I!q{&q63@X zU;T?kS=jfAh)BEM(*MH3htk?1vO|)!l4x-UDa6yw6s*M=8RYSZhh6z4Ii?QCjPd2y zw=cCkJ-vQ}U+D303%biz_~WH(l{)y@iTnd66~z9W1Q zvzTDt12^PsjvzvL&|6DOE3ZG`!s+2wQwuXbB4_mtb9gO`P&SFGfzEaTk#GL5+I=v5QTPk5V7*IV&W{_8aVPSq;FwuDv z4w(hJ$B;vuQIaR|UNRH3>vPZ!4PKc+43Gxmm{9?4kP`r#^Ug@qj4tGR&w`&8&B#3r zp7tu1gU-vU{W@jH^zLEsF1!3Z+=RoqHX|sqgt~rqWc$FEq8FhFo&}ZpkA)IUk^RXU zo}jBg{*8nYNflc@1Umqlg~`%y6y_RZBZ*YuyIbt~ALBZ*e?d@idGM!xvp zTi)+(;y+YX!wl}VwAF?@o+rAyGKZo--F3-;{FS*}z>*_}$+Os%I$MTPz4jiVN z#{aJ;icjwkK~BjIa|&8L>3;@-87qRmlwr5O>VZUjx<7WcGh&-@G|P~^;W(0|kU^*b zACXRofWXAPcsxZ@lD;NM@Bx(nGZ$QijD@bPdjsQ=3*(}WIwuS)!`Pa)o(&HmEx1wU znL@e;t$d8TnAcw4kJ%iEFL3dLJ}I8w=9E!C$zmDhABQ)%Z0Ebw%Lh>I0Z4(2a`6uZ zr1KIt7ay*Cw$$Z+n1IxEV0#Z51ZGPDejd*6@A>q*b#cBlM}*@b@zPo40aSxjxgZKF zdbd0OPQroDfln8cpz>4k&@`MYvi{Y%%Wod!lI}ER3cQz4d$bec2a5~wS_PIdgS<|^ zQ&hd@V&K%Am?qE-Pyt3;$C>)77S@?){g$g)J@QBFNzvHssX4e{`_A`r4mHjSV>uiz zcmXSGE#-0YB*^vB3+N=rww1GR<|KaQ0C|(pbF8Q*Oi2Z;`igM4yR zv8tC8(mUKk#*$U*sFjuVLKxg9d$r|brff(hz!ERV5EPr%yQR`WPGZCgYbnTP?c&t0 zHXOmWT0{uiR?N5$j(D8;VLhhrs>8A2D4qShq+35%H9U@oB!jGs{e6gbC?@CE$i|01 zQ9+JQP8de>U|K`(04$hpa)oIYO->21rD+vHjhU6l!cp^-#Z-+2+n0z1ic+>hXQ>yU zF^(!G2}a`f%(MS+Yf-EI{tFd_(4hTFC_}fIs|On9PFWUFHm$0&-&Ir3bCF-lz;K%l zc=pN~0hvb~^ZNYUq?Vt-2=SPVm%%PQHg*56stN9Y^j`s^W?R)fK#G&Q&~`*$ks8HC zf5B@HKki9N@dNg{C(q92BT+#1Fa{HdC%2dNdLL`HHMwKzLNHR*B7G7HJ``~6Q1Gz7 z%&}V<(i;0T==%@ON0R#>vZ*366^m%VL+{TF_pI_rs*Fr+A|ZK?eH`*VF&Ae zGnP9POyXeI$+n^e5(f*Y1t#es3zcpm;8)*m=H(56N{*Hi{)kO5<*KHDvEsIXvSHUk z^ee`SuhFB`P%uFfaN*{(0MPsju`XJ76KD<|exY0Js&u`HBU8Hi`i2&&SU|-sDH^Zg z%p^934BN(T%J89<0yrLbfS+nbS|=l&i}Pq_4nc$;M`z>lYlkREMbQyaOw4G5B8#D5 z)wFYXlL9l=fk3f4aKV+@H^LLpFT$WjcArq z{+dykXf*3IZBH1vp{794TU6W51jC-EEEGMzZdcrT&?8|FjZmS-XWS{L7&J9UZb*1QHW#F?=R6yrU1{?F2n!QE?_4-2T~o zavNSzfWBKiG^+*>6;lDnSW1LEv7(7_OYpjeK%0x5u}DCCs1%Jc$<1;!tyBK0z}U$t z_SK!>eNVO+=`g2ym2(ZjYrtYe`=!0u z?~zkz$x5dE(P=oG;@VfNA7W+}uFqPJM@FO{cTf!wI}C@u0~-^5z=%6riU~h3nX!k| z3%X>Kf(V6JtPrBHvEJ^sffMNVwhZYY+)>Qw;XMHe zy*#tExF{4gkNh_wy|~l1(ht}Ny^Eh$5DCBoEeUvTkqkD^G?@v|BLIDv9={HTUT-ZW z5p~Wn775s6;1KK)M0kZ>IM0+&Lg>ONrLUgw;eGH#W=Ve_a<2tgxrs-LM@Z(R7} zscmx$qt3><0b=8EfCQ;JaWF}pI$rYB5l($k1rwAvcm3#9{x8ERWW3>AG}63Bg$1Wx@%NKRl<9>zdX{cB$GYuv_wE=uyJ6;xM$IqIBjY)gFIqsk(lx z4n{b-`_N1&t&jAXIwg>Z>P*=|A=r{O0LZ)SK^ZX;YQOaQ@;Q|m9*Y1~fW;nL04eAp zQa}xMNeGwUCu>z&h0A#Ly;ZH_CHu;x49rgo*33zZC!<5~wxqw0RN*mi+>=cV!FZwi zgEzfhCcT$NQIChI+E#D_RcCtiVfAxN0M985FRc{b1108TnwDftl@qk%baVOW?Hl{c zK4E=HiK%TUZ~Y-aFg8tC*xp6_$tXd`aM_|YV*goVk1>ELz$AQCSzKgcRBHt4+hMb62P71LBo5exJF^c282k9!pX%1?+0b#YMK|2)?Jw#Bc-jxZsH{%DHIab_lD=I${ zYzp+}aCKQX-4=UpxeKmlapCaBmC|$tYNo!I*mN(To(oXSO!k?mdrHtN&OTyW>db-w`)w8wm`Xu6 zN!H3R6ecTMDl3siEUz@ukHrX$-(&UQi{&ZjNI*GJ9|3IT36TAp3E8G;FNCEwcZLz& zf&rje58JN^X(UF6;WR_kxjas$qZc7xS2NnP&y+=dT9wm!a&kbk13(VyzA&G*(7kJS zx3N%%JDW12-XSVi^@O6G{r|iQ>;dL=>t~jA8)*~&ie>?!UTFcvaOI8vNzfyP_$%OF z47u_mI0o6Hm<*GT_f`AK#W%>SX>37$S7;r9{M8Nc z978IimwA9459R2{CE=x)8@Ncr`X>&ib#K$wF){I~(P)|6%_cIx8mDO7?0WLPJV#@I zyzE#|#?LJPfV4)ftA+dwhT|b1z(+L7kjA$3HUxJk%OD7hwId!b0dRk=F-7k3(MizQ zKZQd-$;vXrzb2JmmQrsO35{{^V}Mbu7m^g&P4IiA~vP*As`8bfOO>jkhtT^Q} zW91;4LQpeUDzuyrDB0_K98|5SvUa3+{EwPJZjDHEV-k=l;6LfI1rto?Xz|X5u!50F zCRDVTJ%WyIaR<70kM&0(SLPy&{p9s*?bYr!zPmDlMJmCU%>tr zYjW5H@QhX=MI@yn-ys$Vih8D7X5#CjwZaZ#rnm{fxz2y!7z3RD#QaZ;G{TL~yDbxP z{)P%q?SCJ_%|4WC$6UnW{O_2QcEC+A{BI0;@tI(Yc>EscJO4e=hqA8@Uc!KxXxTz-!r`^7eOE4ZOr`K>xqbg!q1M{Dxq34X|Pru%cS-?!W48PT}_657J|8pWZB3?&Pt7|w9$!isk*=Z}ofjpo`FItk0vqGrj3by5kw z-<8=_0?Oz7d%k=3OTQ5d-e{wmCn(D_u&MALuo%{-zpT^~X?uLXnVRf5;tEG+QRP+T zn_TC#Fn43ZdT9PKII1DP)N3g~MnlKT;+502bl^nGMTO zu>U7jdjaQ$f3-&2duDDu1m;MeJ0&gTD1`~m)NR*BdiqOjA_D&ndadG%Xsgq}e|yDZ z_^2bMJuh#9;gK^;+T#JBkpv{u%c28RNy4XhSu#87@xSskEWqhMGoJ5}YF+>4KtNSRQVG;87pN@rHG z>x?$##hqD8Rzo!YUum5-Bb7OeVP*6Gu%B(CV2O982L9BOxZ8P|877tAM}nQBNf%-I zH*pYRH37fq;xG2e3X5HZvNPCV225w*4UhrD^T*p?BBss=LF~K(s67Lo+%BQ5rm3M# z6ps)|w0hkM2E0^fm&4SUOu_|)z1h#TOQnuV86S<%togArU#Cs574q|1O~YkwMh((^ z?kuaml?-B&hl}XI+++E)Wd0wv&_)-xa~WVG!LC6Iy}{6ISD8RtVen`89=3|nlxUdZ z9d@_Zw0lsu9*1bl80=#f5xsc^%-DCr9iSbs7`izdG4l(`-5X&zDh`Zgn`(XN?BWTE zHj_=)zKh&YAu#{+F>AP~RbYcXWLsr;#~UIG%O0$mAKp8ke+p|$lsX+be%F0GC~_uKbaa1{MDzWz+1KSS zf5{5IYqp}vp`|NL7XoNu9l2wmOgs_SZ~)uDi)zOSt4b#i5t*d9ifCg7IwGqEO@8GXWu!w9X=iw%Xa916c4k{+GEFf@Mv>vNq z5$60XdY&r~Hul>8kKC*7!NI+r5w)Fp?+%v{P{S7@ZrTSG5;7m2Ao94=PPX1i2^s8L z@Zi#r^T2?D5)`mL9px|XS(VNa-p`82e1VW=oih>%!uZAZribVEr|kyk#EBRTAKV}< zj|?D`Vm5+KwAzoveKVO|xCIGM)1H)N7@Vf!y2O&5dJ<{y5c;Q;xjGUI7!m4cOc;fI zX9RI)@d9u`&4~7K>Uj%S+x`ZE)F_^TB*jKR0;@vd6>plR!$X4=#mWIc8|6$dP*yVj zr?|_YY8k!?_FpnDt_gSU!WpZP_-#7U@oZVBtHTC7^dl);E`>8E+eL+22WZvZP2(9UQ>JSHi0ry(jq>~~sZp8uzOyUygi za9Mfc*zi)S#)t%ZCLIq{L{YGzcEvu!-`&&^Gn+(7dwKwyTbd5Wa$D=eWP`*SB`sFp zj0rkR@YWSl79o76!P^@oIML$8hK--`kT^hVC|1(a1X9ab02yJzOlMyve?S3!OVEA9 zr7oC`DO%0Y4gm}YGB}fJShV6~Fd|@7Cyp{f6)Sv!*}ns>BGb94t43 zn`I;Dpv~Fm`Lmv)#o@Nqz$--YI?5P}mpr?(&IVJpb^3CPRBjsWA}1yd(D=BXBdTtx zXt|=X-GVSBbGJUf|FhPYd}Bcd+xr6mP)_f3Bo_(R)|V}XApug=k<;?+1HAggvjzb8 z57Gpra$wkdIZvp`a3=}6AY$mleCJRm_|S=WBT$U1hEn;l`x2FL_^;+oL%0g&rh+Pi zwc=zLD5%@+D}mfXc*K4HK+~Ye!q%qZHLwI!knXija6&BonjC>0q zQu?2A-~MHqdum-aWDwx@PJ$&1FOg~q1Nj=(0*lTx#k z8Ky{)@hP|0j$bStaf`+C5y*_T=j|0= z_`7i2M?s<1-_Vx{Up6%X@a>4dvr{(ba`}+o^MgBf`9Al4&8yPZR7H zSwF4V|8*T)IuvNGg>{$n#$auiyL#}R2(=(4Km0Xa#zDDMXJRyBQZR)Ctru%g!5=rJ z<=2>u-M5N|#aNiEFREz$i#D{T8SsCuW`ag9hUY0$k=`wWWzl_-90YSrGZ= z=w(yI-VLz18os8>8@24a01fyaj>uBp4C8x1Y-4Z7gmrhed7X5F3Z`P3sA_m>X&IBn zax$U9`NZ{R;ISOgTtV1cD>WK?DWh{_!vzKHfOBqyjbddg+a$C8C{ulYX`Ok>2{y6m zoQ4@+F(;jSiltGcCjFMcT>s64tM+xmamLF_-IOo5i&@e*2XaT`>WmaP;mKLz-wL6; zZfkVsfD#q8L`j3vNcS^F)w0M_3q$3pX<+6WK9M4Cf;UowmpORv>h!-B+I8H&T`@AZ zm}rAOn+4ox5x$o|2CTt?k&J^Tyw@IQ7?>pV$L-f-&V(OX zbh5=c;t`jq?q7$xHiVJ+)sGqj(*BtW4)L*81qZa`Gd~d|`ccLapIJ@Z0;F|op=5Pt z)%Wf@OvMc|jJttz1VaY3;izx$P8Aw4Wp8069@|9p#jN4#em3w_eG7qoK|n&O)(U}Z zLhxH6?=PJXxj3M9Ar<22kcZahcJg?sB7t4cF)PGW454p|E%+zZF%O>1cOIh6b8X{d z=mwxa+p(m>FwI&R8gsXx%Z8DpG^60)1A81HF=g;~?dHsfXE^X=GfA!ecCm>C;80|s z%0S_JgEvwu^J<_qN<&fm@t!Y~xmbn1-q8JWZsrY@SY){$Il$j0VZPn(_<}V-QXbsn zl`d&})Uo(jnv?}YH3p}%kqsj#;hL~94c^b|3!$XOCvCW%153_#5LLrZgEG)ZeBI+; z5S`UGPUZ}KPy00MsJCJF_1=XJb-=(=3C@D? zJ}O&}!imf9v$yTH3%See2@ZpUJL2(tE?~KITOvut{6-NRnNACsbs!aX~hULD_6lNnZ3DUB5kegt2+y3m}gg01e-=^VBFrH zs6zx7%0{hr7Vk?(K_zoE5jP8hqpnNHYH&5EQ)?ck>m=$GX-`>tJiTIm{l~?SGPAZ= zQJsLJBS!tgaBo?SCIqD274eBDN@rVu>W^y9%HIS*o{g`nW6aDV1Q!Et5@c6&_o1j z$aKwWNp97s&RdiRIO$p4d<(m);KsI9m$l-!`aS;)_p5iMJM?8LV(u)ae?-{FW<)?L zkJ}pWC3$6QBjy|PJxwzj!75OwjHs{jgK}jhR%HDzFpT!B`k(rH2UaSrc&mlPijU^c zLhR&^Wr{MukP#~6?)v@@&syz?@5a`e=IBp*>Iw1U&&9W{1vK6HtMl*N5R68j>farD z9d@U$25})Gg<1M-{#QB3y`eBr_toRW;R$gy!O?eJMl ze(e4$4?!FUT}^GRm*(`r^(lSpPF%6MY^2i<4&an~jN{Ug%5FHjWpg9cljchvCMW`3V$wFZ6h+)EYX@h7i zpIl08-ei#*_rgvO`~|-+YB({Lk_+k3%QWi|7DtQZR3w|qa933OPGhsrktMV`))Cci zp{rlb(yGBC(eDf0o0fk)h0Qt+#oZT|k@5%XhO~-1F8_ z2zuml!#z#-$DABq+L9ZbLxDAEpe{>Z#36#wJa;5X*i|eX^oisy0S_1-YpbzWy0O0rjbbYCNgZj#IVlE36 zG{>3IlDrQEi8v3T5z*We4$9>t-W6#ZiFbvVP^u>q15%omZoJXWf0TOFp3u);Z#S7{ zsUSE3A`M>=az%2Ka{;EUS1)n;hW8?*CGr0$og9re4 z_e;+}R0Yv4p1W`ab%fwPG4{VM;UvP(*gGW;txcgOX3aMhy7Fuz1l$#Xm?s2rbhZ=R zQMv@czH8r709fO8%uihFS?`>uZHUlX$bQjxjd!`Md@o+AT{nw10d!@cf3- zta%mEo$9~5DUl-epRQFBuf#Uz`^_JCSPJ4x?0J_p|W&MZR$E!v2gd_nJUssP!3o=^CjQB#NJY>IvV0s~@C>d^fFUT2{ zmMtfjSFB0L9pS=!I!Oawbz}Ld7&u_{0L8+(U~CR96d3EyXB zW>BZ4s|o?=z~wORaY();h%onpR-ATs&NqLXy{ASnUT2pRQ|n&RUbA0!ISLTCVaZGIPku zVh?zLKsFV8jym}oYRwDJ4qn>3UW$ju1eIUAvD{KwSFAVu)s5!w6TX~SsUOA2AJWzx zr~+SH1I^bNh%eC_Yx`c28z-TsGTnN!3qE0=c>z+Qu68Op1|dVn0h6jgcg$H0|9TZY zFAJ!s7CoIH7%q|fcZ(cmHk`(m{@z5uXJ_NPNafZYUpt&eya9yITi^&b$5p|5(pj_6N^C&<4kg`dzH} zgY(Pn_IvKhuiQc4YS$hh`ZU}@3<|+35I|67xA?0mq*@s~TX#7L^zE+t>JcLh{}q?R z7Dr@SSGHV)tZa1m$_`N6B^tYpUJH8MVn8i#Aa28iIO_Y#FZz1EH9)qc~PQ@ ztS7r#wmI!$*Tp(^j*G5G=oFt2@NJn;n2$8OD(XojO4O??yQo?)b! zon<8ATNSO?G8%Di#&FotSm8<~ibZgims^(!iEsT`^7=@>9YAHN*uatGCvH-sL=6f$4jQ3M1deEzGP7z~Cpk{Bl&8jzG&363x@vx=`R}S!Cg_0#m8H ze3xXl&~S$(D_V^>T7)~XB&cfvD+CU>wf`|kSv+dgL0mOw0ziJ;9TiY-bu*+;c7)UF zuluWa;Rj3p(xF8Ns;K^j;8TIrPfX_=(t-}}C^`u%ksv#3gzHaH+wy^r-ZglQthlPO z4<*3&N_JmdCthPVi*q>*kpAR4J#T~~aNZC01e+>GW>^X1E9Oj!#`!Zbwn4@nI!Bn3 zTssW%6BMH^4VVp2MP8bN1pi+6UeU~-_cLhNwPBP&cjdn6y!c9-TpC08cn^ht>{+%= zG&okQ>e>P|=;aC4?qI;OaRw_sFPXBbnH9n-&f)_0^S5nzM-0YpT}w6uP-~|Dc*?+~ zb=xZW-fduQEzu3Qr+6$(+-|?2b;fSula4IipRe|Gbsm4EzzEeBz}jK>*GvnIr2MCg z<$3=~HwPr!sxFt(RpiM6+$K;#Q=NOOcqg!(63OBH%IgGR=X1BeCYMdH7u^n)uvsp} z7?((Lp4xX@c0#EQLB9C6)GAl5GtT^a6#!WbgqGW9w!jb85C?)QJ3Lpoo}8boVfuEP zFEXn=5B}|COH?0$gJJBx0MhcEKo`b^NW59`| z3O|c2SByj73OBK@hR^6vrQ{pX#!2UO(Fybb%x2>XAQ9v4;Q)mhf+NHvZ?H^*7^Ms(_;%Z^%MUCXLzk_+8+3{g znZXS@iC#m|=}EoWuO`h#yI7VAjDY3aG2~Dx5^!4y4FMgcAHga_)z>VcLPZWcX(U^P7cz zgWP8l3OZ&jS~2R!S|PenJvUm+Y%}%#QFnSRonWS@*N*5fIX-Fv7?fow-ijCjg(}9l z?B`0Wesa5pL?_K*;}WDl8g_RZ%xZMlX#j43^51RWCTQ*dGw$P#rRvhO?E#|aNvd$e zWVl!ZX3o@!@!Jx9{%4h7or+%gtNVHBt%4YFc?0W0SyU{y;pZQMUImEla9v=IP4IsG zSZwsoJ>BGt;zKEbKhg%#{ZoI3-$oyZ3O$4CUX!uM8X{c{{51j-tw*%jhg3I0^rFyy6I`utg4hhE)9}w+8MmVO%m|NS`X`s) z)UB#LDychTZ`T5(W}E4s6uazOeNGP$*W^!?BT4PJ*IQ1je`L82W9?eBP&iC5La_c| zv*c&(xW>nVy+oKfe$>(~JNj5GTEzDxJ$sG`ggLy4bBW|o=pX@8TjigfoN@~k)Sb#6 zz%-^x3A)BzIvb*(vLAR4#W9MF&vf@JE#^qvEPcB6j4&yv~?iD|2ne@CHKCj7OJKa za1km7NDf$ej2Q@?Ia6)Yl=H=!XKB)`pQ$g1{x;nscWwQWzWN?dGw7Q5Ej#Aks?LPI zy|9e>Gw76NGw8J1;^9TmJG6pR><)D%gdwc3dz!giYH=I*Cq|Qh(*41R6a-6_NDF^j z-+XWQ*E|(se|kR-&k~z|UVi`Y(|>+F6W2BaB9DFeN>pU>QN5~hx}RiW;cW|DxsDea zHGiIW@e%<}fo&wOykz7Er8$=8Sez}vZ<+&N*84w@9dDOqeLeXMyPYVWMvm2gt9cQ$ zC~kMb6*f`QLH;AZr94Cwy-3^*!b@4t)m`)oFVXqKye`C&c=3I!NqG7k(1)ReK69Ln z6qQ`)$#ss&Yd#8`tv|FEfs;16pgy@Fv5jkXTlCPGR-r!F{Hr3?F@kP1-UwS~iy>mc zu)1qJFz=3Bvxm(QfC`mwC`kT+ku;XW%8cVDaP0JL@A>n=qU{I=j_P4u12|z^e-L2O zVjZt%Sn6-}^Oq~HW+#kI=nP3|aF@Q4Ehc>MlvWi(bd=29F+Hpn&_NKnAyz2IR$5u} zaS!@h_yfI{RTEvq-M;FJrDs9cR?Qs*gZ#1>gL!=aE#vZBA+0fdxb&*EGro%j{Lv2) zQ#W|y8PRuy$OPx{SGfX2GP#J#P%t6&w3(_l7D|Eb3}|oWr1S(jIq!b8Uha;dSd}xE zXHu|M2eBL2s6P;xpWNCLoDW2Y;p;_1;3zIUV%)>D9gP5c6KBVWF+JU!6%H$m2^Uh6 zLT6kXkokx@VEXZV${)0)vn7rRfNW3%x&P!8QMh48btU)pV;rVWZc!Sbhqy> zlhCD$WH1s>pNu6Cbq9yJqg^f-I%evF>nXH4IWOV6&z~?Nng3^fFYWQ{ZQsqcU=U9e0bL|+9j~E3 z;j)GECaT=)M1hCC#%sYp+!NlQb3djf&RU8-yIrN@3s(`Lsbf=MEFY+`0Pt`P6!s^3V=^0Z9{oCMd zO*K0&e+4t&kDNt@Dw)n8b<-yG4oA4eS8ffM6q5C1rMZo<&fPo6Zd!T$aKO|(nN)Oq z#va=B^@vNpI0{Mw6{Y^uT=dsO2#aTZLh8|xM%5O}VreaX-=J55dX21c#o~07=O)3h z)?~iYTe5_>ax)N`!H;^zsrNtXnFP`kn-mVymXwWBOpUoasN*tOhLnCt8%VSyuuhVw zw*X({dKtRM1+3CPSpV_Pa5&NQx2|V?ovVj}tSbuwYW%Xn?oLs^9XBL08 zrcZe33qiu-qTy7scF#5=?6}W4@}s0(GD{HAIH{!E0RHuAxvvXbtEqQH5>CR7s`)xS z4KCP`AZmON`89*AFF%bXA9T2QF8&aW!4Xo4uJ5tq;KHVe~R=G zo_z)Yq&X^IPo>p^W$=%MuTucWPH>ahu-r_)J9v1K-M^SI^T7Tk5zQCb` z{Hy%zzkuA(-cT2BQj*k~-vz&13MMcUd z*nf1_f14%CC@J!wiT&ppPM4XLt?yJeNIgg;#|N|S_QUMGvz&J^cpJ>gt)Ron^EQ_| zqOXQ_T+p%WdII@6tPVp{>8uRq=H&^h@u0<-AXu{|4dRlSj$ER+@;nG(MoPuDoSH|9 zgwEW4{FfK33*V&Pq6Oalu>>P9fTm&RUZcYC{;h=|QEI_&(iA<;`7bI6kL)x24sNBq z_ZlQDFvIyudx~~$IWh0Jg43}*wv$}XxIPs`dB%QezyE?*QRewU9`jKx?4k#bT@C^1 zWa$>i=4p^~$1fWL%2KmiLvz{jSx02-I0}TynvNP^LFy4hWSrdjy@f@1X8r!lv=y$^ zBrB`NHjk`_FHR>srGl=ySpd^IT{1q3J z>h)AP;q2I^Icphp^d~6Yo(7xWocy*Ocve@OP2JvsU&^RY_wy1^Cmk%V(t42tyC4a} z%o}%>Nwxu{Q-ur?=2xY3n{?pPP*8lwfOi+`e(R5!EQ-O%QhbqA36P?(bC~_k!?dkr zU(Ds@fd=V$Q88u_evWnB^u>eA+a?!qBHzCPrg9`sves`y2ZoJIuk?laRb5ef@BPfU z=^auHGh<^s(~2Pxwm7lVK0<|aK@O*M(>Q$%Ha)fKSYN7rMrqs)^&iX2^ad- z=V@`)L+yoGNv7ev+QMZy1|}t1Ln%z&0Ka0DH%D|??AElKW3UL5SwzD8K*n|?5sN#n zT};b)>Lmiq;ZtZKYZ&%0AAArv=YrE0?^$9+VZiA;11qA(eTLw2=>{v{82Q!rcHp=k zeOTW7A+Qpv?X+XPcs2vzUZL$)LQIGItL#1Uzl}tEdyFAo%K2_sY&!ABO*rK-nKW=t zysdQnl=V#)8Ugv;eLvv~YsT#@@j=Hp6Up#!%e{F zR_lQokBn<`^4@pS4dWN=Mer5vj}2Osl5;e)0~pNvL&f1Fv&MH!Cz3#g_Fuzhk==otGXGgCr6f58!rfNh`WC3~8Lj|P zu_c58voO)p6^%ulJ(`o-2#n~G6qN!P*D%j6$Q6K&?Kok|j*j1Th4E#UGNd+ClvJ;N z&FxDj`qPn^A3A9V>NeMq7K`YjalaR6rQOg;FV@~QKKJ?lDH98x}il>sI- zs*S*EwwI%k(RT&6kMXt%dcGpS8gRz?OJNa2L}OH^fU-#~l$4ND>t7N)={GXqG=I5C3#9UK}WMs-TFJRg$FK|Kol* z04{&J!Gcfn9@u(&`*;R)peU&wIWL48c_UzUmEJt-cjO%Ipt&oM5XosQc`nEH1f>A8 zo|s8Bu{Sl{0f>_dT%7QZVxlxTwnQtzF7Y1OYKP4*56L#hQ&J44*dZ0KeCE};(Ez?> zUc5yL5HhhMIoBJaZb2Hxj z3zXZh!&q1#M==)84)KaGZTAC~AczJ~n`QA0&M`=7Y^-pe z7bFSQ=yK64>QTbc2TMqn_H8@#i*N4Gv-L8{m3bMqM;xtiMY}@%fscRN#$r3n=zr=y zPS-t{@{1Hgk3qKuU^Rg%VcN-MC%WMx&Z1kLlluQusW1e+Z2EkZzj93totO)=vfBs- zK+JuAGF^ZBmHimYaRi=T|1u0#O$j)Bu-^LbeC(H95g@#O|I+_a`>O=pPJG|rd_TS3 z5ZK%L-~Foj_H@6!C*b!OHNx9B7-&hLGvi+NQvL+SvQaAteelu!hdNaSHHvN-T!VIj zjysme7ae8mJM6zae#W!bGsPCg-!5HWQCM;D%&EVPG7rg>qP(umzjgz{AU2Rgj&890 z6yjCLVF;I8wddnb>xDTfovmdiZfd1l6d@rd6=!$aWJOfUB?Qoj8kJQ*0;j#QZUqqm z>6O|Kriq!U&|GORNxn+SAOvE|2o|IbgKl?J3=B<7HRO_gT{Rd|$y)IXS{@^RUHkoy z0^e3q*&w2qbWqC70u*S@-p_PzWcJr0;LhWz0v|`aFe5ySzE|9}s*W0=s@x;{fJxQx zT+6K#^HcKQrE-xJ{#J7zt~7PpptJ2+tP3bA=nAV4Pz2;^Y0GeU01x3wgc0zeQ-#*X zx;jWrLK3n=ijnhw7%wT#fl*@O<>68e67#2|IzlpVJj>dqVZh5S&6E*hJR+5`(5`0b z0aMJk`d&<0*6wO|VkYEv)g+l!np%i?evCtYc56+YvfSKh?VNpEreokg@VAe=34P%y z8t`0nx+0!>C6+>@kU%5_0oQ*DmoCrxQ{;|@nU}#wEEYY7hp#tN0t~D^Ez~_{g9q%( zn!c|(LWqq1@y>j*b$?RMD-80kUBwt9c`JB@bONe%{$0?k=Rj4{&G{xjqQ(>IoN&Da ztZDNxJAedGKBV5ro@D46Dzv+O`3du-J*$Iy{PGgCxav{Mzp|Ui^-u7<&Z0%buwUtU zAW1kbkZ_|mosWT7`}}t%A7JtO^TyZW1nB!YAQPjmI3gfIiAzFwqy0rM&qucpX>SN; z=QXDA=AQ>wM*yB+4b;}qpr6DnGM&=Qgr$+%#mc^uT;VSZGi})uN*?qvJeOMrj=yG9 z(glspP3CMN^{Nm#LzZh3j6CWqZA59hFV`}E(Z5NBDmSH;+J5hJ09)1rCjrA#2Zh?a za+MTI>cIFYE`mc@m6fE>3QNP4MW`wD^AVU^xs}uqVo)Y8&WP`3;iyfY;DQ)Jg|Uba z1gk0{7K);4v0^DHH0QrCP3suP6XL`hk<1jkh8bE9jb|o>_939I3ajwGP~AMg3mkb) zFg5ZKOu@Q^(y-GFA&7|l!gN8-hiac~n{|uq^U1301|IBfX&EnJ)Dbv(31A65TM_G1 zf{MK%xCN7TaA>d^%|3AFPompKoczNH-j^VaAqKoR>#EB-yZ!-_4Z6{4#JWt6Y2b|b zxyXac-IV98b2*b--^*)eYtsjV2=%$>ldGG?1bV5!w^YB<2@IW=MBxT zGX!{ga&>=ChVvpaDj1%uoGU<9%JeR?m=a0g@6>>H1pq zp>dCPNWx4Bdolwv3uuC?o8ny#?!;5oD_^o`5DOw!+WFNk6NkyQX;IIbEiafI2+c_h z=FK!x(Jm)^Ry?tY%v?#3*=343DfK9B-E$W>fhK7Q^AFHTtC*f(QwQSISufVh5(?$X zf;7{|EB2aW6LFQ1cxE-zIP`adY$bw2B&nh56;L8^hk4Zb4@ayZ-_b0zurZk+ehdCnfurn%fI9+7mM{y;f( zPaX8l9zu=pR;>76o`N6Y2ev7nVR&lM>UICN6g5+TJI>X$ozbBruJQu;(dP{4gsLWt zf&J1j6!R3Eo*!;OaHZ1G;0R_@TpMmMTq4{2aE5Y z-E3c7XC6hGsJKYrxq2H%W+8OgM1zRzmC_7me+vtKZ{_gKkl)Loz(?d_eRc=HJ}evc=-9 zA?yQ@!8hnzwOo}~=`7-+&z>_TbS*ceK3AQzNxpbz_Bn1z6dscF=73jX1MS3$AMZDO zZFs;d;semhL;9!xbnvK+8;#eBdsVpv$m7BCRc{aRMzdFj>LDYFwtA`9&if%EEWp;^ zF-`J^->P6}jjvTeMLd}pbfU~d>yh7Jg-G_{Ae;W?81`!F=y*Q0e_(KM|RI^#Gi{LJ9=Pbz4HA2Ffquv>YFjJ{nC1GZrhc5mKd~5T3j~H zc5d)5@)>y>%h(Gs2IU!|fDa&R<{qI$h{!z5nrYflECBspwusUH)^oN|E7Xx#whiVs zg=>Nlw!2o8dF9{BdR5Fdo~uPv_&ivFu!%M+nnJLx<24=7&7%7|m9}C&@u8JPhx5zc zuDO$4NB{pU+B6)!oiwf0zgADB(Fy)c{O?(t2DAUH{OtM}Xj=QXZtL~`?fW0g?raVJ z=gdb7yXMPFH@oKQ-2V=&aku>(|MYX%($9=KTEM&c1V$k6S?Z3x*4}eEOi?4|G%9Jk zM7rvr|F8g?R1TzVEEUJ_PBiB9gLj4J!G+{ORW$2YpGnL?mbj;rhN+WcsgmW%K)pbCk~tn zbG=t2D2nJEJ-m{R%-FJl^dlrJg%34#NIEHa zbaq@T=0gyZxTX%X!OY4loAHv`&)a&Nh|nkQ6<=NopHi6>)RVEVL+!Ole5_2alCIGW zwA(FrBudWKJ?70^ra@PFNCB20>Ug~G{I??~KvL3&jZ>9#&0aR!5{O(Y*54^&P0|T7 zZ$Vtyj@?graDnES1(9Qkx7d4AMPid%MApZ^K&JlIrFdc33?Iq}wXxyc$mXe#z93axdIu#CmD++tam^TSy zhbNmXazW1ZLC~2rd{65{8|->C<`-UwLYO{5w<>i;R;mHgM4j-qz}zI0`Wi-1+d#{M zQvunS!#E1**wv0E!oMhJ!vn>_zutb2i!XiK5@loFw{P28fa|< zVqV!McYHj(FP`J6dfDGB+;V5@Z;>*G6Ai|ZPX*dZ1B<*az|SQ0^cFI@v?Z9m zvo1lu{`)QBtm(&JL1T_0Muz|)F7kpL_1?D#Uf$gI8dhV9yx5LByv=)3r~$VH5pkgh zo`F~LxZ@1?Qs6oa05Rc05AX$QL57>=uW0e$_!ZUFqroo@{UVH%949nAA>iD`s-Gr0 z%87FXfRAbb7E=!$4%Mly52!cc^KM;Hc0=si!n=gShkM$wugmiZ;N@HmGf3X>=f1ip z>gl?GYe=b8$Dm`yaVwpNe_+XtSk8`{ z$%#t=pS_zZvRP~ck#-*b5jweC3RhTHg|$rn+k8ulTNdl80qKT0%0>h9udQ<5?~lO% zqvvPwP7951P80CIh&6ztVBfKbz6{`HVqA?y5sJ)GOs1E-1KNXvh zV5!y&g{>?id#f1}<(?SOQV1sV;9Y$HYLxchUkHps1>=c8eedSKnv{J~JjF&{881!&S3630x!3MOnpcbB-zr?3ZLDh9NBmXhOc4nCJ z+D528d{1D4YQ&iL*dndO0b>CMJQLA4#v{`u#IeY;`zj=k5fN}EAZa5F`;FBg=D@Mm z>Ly^)Yq%Jby54lXm9Sji5vwGN%gpUEEEFmmFN_!h3B)GG)pz7l*5okNGUZw5`oTM3 zV3r6Z>kFw4mZeIeA)?)&M@yspBO5AThUS?~MY>ZN@rTsv&m1R1qtTlU-8;m1PiQOt+RM;*y6iHFY^(9%t*qaj_-b_Ik! z^4)@)NmVY*DxZ;adU!*WW@ddy8^7yVVf1&CC_~TxU7mv0Ftp(#QZ!{wjo1nZnyn)D zh6KF@#iG<(6KI}1U%kFU^}VV!Lydyh)U+lU{#L_1 z<+?n~FCd~{tFT5|nEE+|NHs*pb~b@-6ITQGfg*cc!Xj!f1J2f^j<$UMz+TJJ%yeQ~ zxPkd3FJK3nj;UfJ{lU9Jg3jGS)uv&_@B!0c07}i=Y?PX|EmNOX%_B-W-DHHxRQgOj zVv+{tYdChPBRKr~2s5Nhsnvr`=A~N-hGz(D@%~n4_gD#~4`X=xM)XSUf8N*Zp&1-0 zS7E(3O#GlxhGxfcuU}@=m*83N$yMKwtEV}Ui_b&abu{u~iHOIXbj~;GX;kBLB0<47 z7~J)f`1y168&mQ!VQMbl`$Iv0BV&~z;5StmN=3PH(5k1_tqc;}IW zj0}b4q%PHnqTQ*C>9H#&m^hR=A_q>EZcdqek$#1i5d3#Ehrdnni9iQ3n6My+xXJ_9Hy0A zy5wo+kdS)O1iV!+nu3Tv# z+QgRa!2}35LD6;uGxkMUr6NLP{*&Y;^KjnZ#@9G25e$6wWd@Y&im}q0gG_kK=ge}a zRzsaZQd!U+dFnj+FZym`H<+`K?v;?hKaE*8W)9wBtjaRJ8C=4WWLA@6z&hxoapc>W zKsMH1^4Bm#_!)1F*6I$lRxgDjee`#;cXP7kqKoTr+gP>>5n=Z_hT_^dW7J~lAQ1$U ztNIMe6$&Mfdr4IpBa4T){;D)AiiQnwJ^12HzIHVAze#Tkb5Rds92-`R=8a+nROT$z zcB+WCr{4C`efDh((m`@~Wm}Po_D3=Rd&_LZOe^_S;~G;uARYzpLQc!!{8?C$^U`7XTve)7EbLJpRrj=HKpAz#wr?g>EIz5 z*AxUPn1FS~V>OmWMm=?|=p21FA}RF)Glyq-2_n>V0Lm$eQIv&gY^HSi!k{>_>CiPb>UMTfsKnk=Qkg8l7M*{Ni z?JBhMkQ1EfE!vErlB|kAwXu@L-neD?it^DlHvEO{kF|6QlNw1E^hk^~W2o5CVq?lT zL?H-*dK-X#8VclZKqZq(>PQmDai_2Ll7&4$!OVz~nL6->ysPROT6G|ECfze0 zKVQ8K+lokPPR+a_82Db+%YgS{wqjvn$KC09zT@{w7grglsm>Hs9)BbQ}k7mj` z8B+`e(iBW)CMLlE$RC6?c=PZ+E^6`$MXslq0-qO{iWi{V!Yz|p=-jU ztYbTm%@J9NJ}=9bj+$hap0b^$#z$uR85nHU*#9ebg?WRN%+cFqy7Pbl@!c0aL@qNH zh%BjXt$NTK2ZYV2jp4F{osT_;tI#i^JiLH)RgRva;xp%mE~I7 zUolH=&P_9(Jx;@e^GJd_nL6k;gr$r-6w-|6c|;Bzt3-9c=CGh#j0zc3*Y;G*cpzM> zg$q`f6gGLR7PE|)7sY_fRkr*ByscbrZosu;Ly50tD-_*jK3EHDmOZ)>Q?za@-4x5N zR88Io3+vz18i1Fukt<^Qavt-XV$$(ck)YA2Q?C0Mw*4bTxz@M&S#WApfwmrhx*4ty zVJI!VD5xQm#DJndn-g-1BaPdI{f10r=#q0iHq5Q3SIyA@i8^&ZE~y>DY%dM@U|?tZ zsQ|5fT3ed(!#v3u4f@6sv1?zPt6evkW<2{Uj;Qby;Kb7L8SN~a-9dF{aWNrXQkN54 zcmhKjFFS_cBSanf)yAIKi!gER{!#K2Wedw#46Tez)tSy%3*FANWM7=V0t7wT&|)GZ zLus_Lv9@t`qAeN+Wt?B{KC3Fz;K}DJ?2nw`3pZa{5L{{LQck@ILZ^E!IsUb^w>) zq!#Z&mYNNRImcv5uSg`P>S^FyO#smmGjUyp@nJ11C|5qKX+dqb_Q-gTY4nfZc{vat zDNxlw!;ah+Ii|FPabh0O+oJcu3d(mSzI^C>+(j?qAmZ6eAQx4NLbu2$rqS1(GA#^+ z{rVWcj;DjE_=)sxph&@%`pl~l4GxLAh|nn7wB41HfDD`N0sTmCk6OZ+Jfd*A;TsJ$ zL-ZJJls@MWZ>e?03XI|cBB_+H%}*Kt@6Ls3ST=eD)U>6GQp8`KQi}SRYp)A8a~BqN zmD0B{ZQq>hQk=gu#dvwF09V_wqHYw~U3{Eh^=^X%%gJL*` z$oKl)P7W}{IAZl?0ilpU*Xe!DU)%Q8*v?Zvf_JMMP~A5JMy-qM9OtoqM z-2Md!nLDY=$CofYH;P2x zC22bCz=Z@?C%)(Hqtw#P2!MdDmJL(XNDs3?+GWc$@}voDpr$2=;zrt})6q1ZN>eMS z-=hJYSpo8Eq9+yJy&#g@(pQxGHst99mEiZ478?Q{QTDd8;qA;Ixt!Y{uf?{KKt^} z+?ZiofoZ#W0IBv1(?-uBu)i$uA2fOT&_J=B#R5rUXz4|@=rqM6cw5WB%1oA%ysZMz zU&9>LUhvYB1anI~J_J&d?sTetON2n&e|pX`(jF0)EMb~$osPSRFDF$&w9}#B&sKZ7vQ($y$#G^ zQNYW8q1CWu*H^h(i~c#U-+k~^sMGbdaQzc^PdEG2FTgN6fW)zIPYeu$$P{R%#0c<(RP?&n3OczS1Loohs_3G)o)d5|IX_9V{at&_i5Dt z^N9LVco|9khVzCB)C}}avLE8??kD3&C$sIgI)@j9t%Fqv3UPfHNUt3Y`Tk*MQY-n5 zsN&(8lh)vsjb#tCd0(oSiD7TQS0dy!`JG|jvZ#t_m&lU!@E{Zq{~&|4uytU&Jdi)$eIN8Ga9jl^xa=YFk1>s#?nzQYI-4KHH=Q2fmPsj1?9=K9Cc) zduBgThc()vt)?S_hdq3dX=SYUul&6hK|&_Za~dv22zjhbNLX+}cK)g#|2}OF*VL8q z-^~Y`>*!H9j8Tqm^Zdr4oKj_cDvvb7Kp4oVaNI1@ZC|MjUxc}`-UFm4z^>b? z|Dh~@kc0@NgM|6iphHUQ3=36*AyW~$FE_(boA~``In#BU!f-KmFcOd7fsqUSI0OLRz#pg=Cy z*px37RbS>L1J?q3w#2MUl8vjcOI#_Ve)!uIZ-$GGaLDUV?`jdfTAU*Zn}xjUaZ9TV zp_-^0T93{f;Mb{qqf%hqR;8>+{G|CKX^>j*9el4Iao;QOZDIWkAXEjWWVF4(E8m}m|_!7Q!jtxQ+3Jd zmw>%Nu4Uayo@GPXD0f=+^2&(E_>QD=@~zVH88xgKKb<`m{@zZt90Gke;Qob)Xb)^l zo0uoLItE`hNhOH2Y%8?1Vd!FgS*^KN$Xr}VJWm-7v+oMY<^TM46y3nG57U*C|DgKx z07vSJIhD?@LD8)t>!@7bifK62(i%g?4F-$n7Ba{C&7mqRgYnP7hNk0 zHC$rClZXVY5w0WXi?NN-0GU^S*C)+ikTtM5ma_YP*}h`DFh1_qE)pqW`@Qe)5u#_wn=-nvf0C( zT{SX=;&JcjBznozDV3YzSIR7tKLE%}QYMkC?vxubRsgkU-W58RV27n2Ocyl~XI#;$ z<0=&WcgQ}cq>KwIZa6fZ9xT&eGQuF{ik6Dn&^nJn$n_D2eFq_U!crv5c6MW=-f*6? zK|Jz1jRD+QUU?l|;nKlV>QONsXy^?brJ}ZK$$yKMBqaN3Pe z)eR-X-m~H`?S^@a#%?&v3~R`*Tu{!@|1@MM$7&9g9%Wdz%0;L74?nS-ue1YLQvC)} z<9Q>rz&m*wt|aYdL?lkZw0w(Orpnmbm((()VUPxho9U#e_}gP66hN6gcs> z)`dOFdh8-s3e-^#29#RK6tu!ph4ZvcJKAniipJ5VMYcGCAzSKjJ^ z1LWSNx_F}czoN9<#=A=RK1&6#h-l_crAOeiH7P@B{}3K;&foA!I1%fxM6ja4oLu2i z5FyVbP_d+|JFoL;uu3#lkij{DI8Bf6H}yu_v&c%E@Mj2*^le+Ecp>P$sCr47sptAM;uyj(?HeDkayY=T0RMhDhb_ zyBiOQSvb7l-LlHA#_aNn`~0{wrw@Z3d^H@H(_G~|7J88n842^lGP{kRw!O<$= z6e&b}m041}od`6w`!Z*}9Q7vh)Y9n`hm@RlfJ8l!B^tk;>M1ZR^aYYAyS#XM; zATv%ts2Wc=S>{wv2!yd6GT>w?I;GvgjgZV%%2{Oo`F=sXVu$(EW8&GUQi)AdZE`rtx+=rFLrd{g*$%{H^oWR6jf$XhIHansDx=}MeVFvvg_ zALstLBu^yxzM0?f(xq;=Ag=<^rG&%tL>bR#j^0zxeNGPO$IiEv^}!+dJCVFn8nBMY zQM`Q$1xr-OX=vgJ`%~ad#GET;i9}r|22G zYw%_v{4p3IYbXs&};Q%5et1M>}!m^LM>gJ!D? zA5C3{Zrer?F?icO5%4GTCcnMquE0|f+r(9jl87xF2l~rUuMRvAd~S+?U5uqiwM+*I ztQ4_U9K-4+zr#YmGknG__Y2GzV|H_AFHXkuNs0@b-CB_GzY;rWjC;$SnB{nt;MzV=Vqbt%pfB_bijsaeD^%YH z1LQBm{X2M8ad@#Wd$x=Pp1(XDYhilGTRvUaPek0M^VOS-wABX~5(H=6Qt4ciQLKJ~ z@-F1VM7!!Ho3;uv;w?m-@VLnasWJsQISW`wo6Li#_gV>#P~A$?YV=P#ls;YCmBT}s z>#oMeRglt9{cdaDaGwaIo`(gcs z>=1+8yFxv-DuPFX^Q0ASL;Lei&(=NP)0R~||F!fhNumCtd86Z!SP{q}Hr-i|^r)4w zz?D=(>?i=+6_!Rel`@=|mew~RBpW>*N~K5x{~N8u8(KZw!pH$7$i!vZd>Qx#V~#lw z5@0fkVBtH*P?)>g-h4H}U!}K8=?MMcbFYKBwylzJQ!Inb!J7otZ)`|v%C6EKR4pmX zTu{yfF|<$tdHSicee)bJo)~m6RxvjmV{@X54s`HSs2FmuT`h)oAP#mg{F{0epAzo3jEhM2%3d>F;HLKoc=g*{^J(I`W}zEQCMIiJF6sZro(MVWVF^D9IQ zlZn}d}41V_|>C7|sD-m<^# zY>xkzkvZKB*JK;kHGcwzKF*kHmm<*CP2odk?N72HmCc;xxv}OKnyK=PsmFeeB{l}d z1*FCO8p^IM(5_EYLkgO78F(cOtn>OIWFl$EeQDAmg~$&a1j}PRowXv^bQVK=KqSEf z6;?q;LS)p$*UKwDA%dA0%LxfCUl$>SYk{N^w=s>N#eFC$20vm5*0)gf3nRhyzF)ph zg3PF^!x&nu4*+@m&P(|3i>Ie=ps(7EmgsOoOmDDvUX3|O?g~7ydS(FV-*k#T0BCs@ zhBmx%v@H{#aVrY&*+geYnnx4SrwvV|^e$<~hK;OQlioh#mRnE$e%cKTYE0!Ec+m>B zFwf))JuzMZ#}8>^$EL~@9PE9z7BOBjw9>^9aa%zWJO^C8X_HzJg;fH}gMI58UF~hE zNjwROOX|tEUfQX6D%igobT+hNVs;V?9Xvj{a?3=z5K#S?81!VuJ+^p^>bjq2OhOJ~ zrtErI-Z5L_H!`Uj+3o{rqwMfwyDg(aUNW~K^n$OUHGa7o+n-qPjsYI`g}03oxOY+3 z193MoFcxgKzio67>g-izkUQvlI-W@nc(Ql&gA%iZhe**X35XR3rbXvfQ`?zkc9_;h zX7@vPe~nR!>zCk`!rG*86HGR&aErFOT7D=y?C5CJ{9h9yXzY`uOMkKJIVRd@V z1{DItR|EQG!XtJ>HhGQK#(xr$_tqXBH*;&glHlG33g#PG0oyMdywi%2fO_kvX*1Ij z6)5=$b~@M9q$F0%O!v-!ue!g}PrD}!?BCUX8@ji}?_xD_N$V7}P5(t=BGb0t#`^HV zGvnFDT9&jTv?S2&iOAvbwN>Y+6PYybp;>8Sm0B}mV;*)HM;1Vy3}vMwR$B9M#bP35 z5IJ3jMoNYx_l#i74vWEBMUYgA=%AbooehhcxIcMm%y-K;@?fddD{^lXtRHghG+c&5 z)zC;cJKZS2A0c3n>2BYM59`rKl!a8ZrzF%8$_58D z)t{h=kCCdyWdm-c6=yj#V0ZC#*u70BVPHk6BTZ99u&vxLg;TeBZ;J-6#MN!bhzg%1 zQW=jk5lgMb}%RsS_@;!<=mR5<`vYO9Hha2mKi zUOmvzTyy6hQYnbF%2lDiF}%+_+q2J}0<~84M5#+CYzPIs5w`a#a@CE$L0w&BHGqAL ztqLObg4XsateUr(-yoVS@+<)I@I`O+QWx8Z@zVi^_(R!BhA}2ojw0ug5fqyQvm|bj zG|s!2#D6u04_x)_7OaW5qL@p@2U&qX>RGBycz7;Vw&zR?pAFUG6DB)`e)KLjP+|?O z=6!3Xhe>*k7Z|*D%d7}}+TaSw^q(TpfBF`Tv(WoXoNig$(&>X6HLle)suS}qu)0G3 zuC)&fv#wTV-?FZ2+3hJSdA_x(9D(uasbcyn3gH**ox^&!zsCCmErIh9bOndxDzZWF z1>tAWQ6qS_*Bo?}3!G{RLiLYemO>X$Ag1hDBYC$AJr0yhp1uN5$sVd@^@J57=0kJU zm!SBrP)Ydw*`WEhnr9%?DrWDEP-z~jwY!98>ZQ@=q%G87_>7=2^x0V-W-3z{9sq^% zJ;dR;VM|POyADdrvkSS!tzu3jFkVX>g((J+A_|0-pZ37Ft|Fu!C7T5!W%~suCdv=9 zE&}KH-q+G+A_@uTzkZY{z@iGXiI(xq=;g@BVOvo|O8pi1$Y9yh#7ezZQjiLQACP(L zk`99yN7d2LREC>+An?5?*`(!kC<*zIO5KNH9m=RL$dupi^KYYw?1*Y^R$+@7bD4)n zeJ2g#Cd$BQf@O3CohkOnSE0!EW3Wciq&p7gZ3lj;+k$4T0*%>1+MGlV>Vq(Ic3RP> z5O1TB=KP=_3e#u|;X0Z97aS+{6(6pdgC#NM;fjY`b2ci2rIanw%F)#~Yo|$&41h@D zqH~dDWmW6~PW%<=;ptyeCdRC^yJ`iIP?Pclpj(>pGr!SW_z>sb&gQ z2~j2jnC_XvhzoJ@50HzDHu%3+UF@L1$_Z6ANd?mr=n3 @<>N``{@sw-Q^NwEJ*n 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 FIfZ`)C_AfhX?+Ct-*tD@t(^OV^~ax)@K>g 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%0AUrZ<|+Y%TTm96 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

^{mJ_)?qz_n@jw3=wR7vBS|oS|(HglyVmpI} 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 diff --git a/helm-chart/templates/NOTES.txt b/helm-chart/templates/NOTES.txt index abb5a5e2d5..b409563f5c 100644 --- a/helm-chart/templates/NOTES.txt +++ b/helm-chart/templates/NOTES.txt @@ -1,97 +1,161 @@ -NOTES: -Thank you for installing Formbricks! Here's how you can access and manage your deployment: -1. Accessing Your Application: -{{- if .Values.traefik.enabled }} - Traefik is enabled for ingress routing. - Your application should be available at: https://{{ .Values.hostname }} - Note: Ensure your DNS is properly configured to point to your cluster's IP. -{{- else if contains "ClusterIP" .Values.service.type }} - Your application is running inside the cluster with ClusterIP service type. - To access it locally, run the following command: - kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "formbricks.fullname" . }} 8080:{{ .Values.service.port }} - Then visit http://localhost:8080 in your browser. + +{{ .Release.Name | camelcase }} with {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag }} has been deployed successfully on {{ template "formbricks.namespace" .}} namespace ! + +Here's how you can access and manage your deployment: +--- + +Accessing Your Application: + +{{- if .Values.ingress.enabled }} + Your application is accessible via the configured Ingress. + ```sh + kubectl get ingress {{ include "formbricks.name" . }} -n {{ .Release.Namespace }} -o jsonpath='{.items[*].spec.rules[*].host}' | tr ' ' '\n' + ``` + Ensure that your DNS points to the cluster's Ingress Controller. +{{- else if contains "LoadBalancer" .Values.service.type }} + Your application is exposed via a LoadBalancer. + Run the following to get the external IP: + ```sh + kubectl get svc {{ include "formbricks.name" . }} -n {{ .Release.Namespace }} + ``` +{{- else if contains "NodePort" .Values.service.type }} + Your application is accessible via NodePort. + Run the following to get the assigned port: + ```sh + kubectl get svc {{ include "formbricks.name" . }} -n {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" + ``` + Then access the service at: `http://:` +{{- else }} + Your application is running inside the cluster (ClusterIP service type). + To access it locally, run: + ```sh + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "formbricks.name" . }} 3000 + ``` + Then visit: **http://localhost:3000** {{- end }} -2. Database (PostgreSQL) Access: +--- + +Database (PostgreSQL) Access: + {{- if .Values.postgresql.enabled }} PostgreSQL is deployed within your cluster. - To get the PostgreSQL password, run: - kubectl get secret --namespace {{ .Release.Namespace }} {{ .Release.Name }}-postgresql -o jsonpath="{.data.postgres-password}" | base64 --decode - - Database connection details: - - Host: {{ .Release.Name }}-postgresql - - Port: 5432 - - Database: {{ .Values.postgresql.auth.database }} - - Username: {{ .Values.postgresql.auth.username }} -{{- else if .Values.postgresql.externalUrl }} + Retrieve the password using: + ```sh + kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.POSTGRES_USER_PASSWORD}" | base64 --decode + ``` + Connection details: + - **Host**: `{{ include "formbricks.name" . }}-postgresql` + - **Port**: `5432` + - **Database**: `{{ .Values.postgresql.auth.database }}` + - **Username**: `{{ .Values.postgresql.auth.username }}` +{{- else if .Values.postgresql.externalDatabaseUrl }} You're using an external PostgreSQL database. - Connection URL: {{ .Values.postgresql.externalUrl }} + Connection URL: + ```sh + echo "{{ .Values.postgresql.externalDatabaseUrl }}" + ``` {{- end }} -3. Redis Access: +--- + +Redis Access: + {{- if .Values.redis.enabled }} Redis is deployed within your cluster. - To get the Redis password, run: - kubectl get secret --namespace {{ .Release.Namespace }} {{ .Release.Name }}-redis -o jsonpath="{.data.redis-password}" | base64 --decode - - Redis connection details: - - Host: {{ .Release.Name }}-redis-master - - Port: 6379 -{{- else if .Values.redis.externalUrl }} + Retrieve the password using: + ```sh + kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.REDIS_PASSWORD}" | base64 --decode + ``` + Connection details: + - **Host**: `{{ include "formbricks.name" . }}-redis-master` + - **Port**: `6379` +{{- else if .Values.redis.externalRedisUrl }} You're using an external Redis instance. - Connection URL: {{ .Values.redis.externalUrl }} + Connection URL: + ```sh + echo "{{ .Values.redis.externalRedisUrl }}" + ``` {{- else }} - Redis is not enabled in your current configuration. + Redis is not enabled in this deployment. {{- end }} -4. Environment Variables: - The following environment variables have been automatically generated: - - NEXTAUTH_SECRET: A random 32-character string - - ENCRYPTION_KEY: A random 32-character string - - CRON_SECRET: A random 32-character string +--- - To view these secrets, run: - kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.fullname" . }}-secrets -o jsonpath="{.data.NEXTAUTH_SECRET}" | base64 --decode - kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.fullname" . }}-secrets -o jsonpath="{.data.ENCRYPTION_KEY}" | base64 --decode - kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.fullname" . }}-secrets -o jsonpath="{.data.CRON_SECRET}" | base64 --decode +Environment Variables: +The following environment variables have been automatically generated: + +- `NEXTAUTH_SECRET`: A random 32-character string +- `ENCRYPTION_KEY`: A random 32-character string +- `CRON_SECRET`: A random 32-character string +- 'EMAIL_VERIFICATION_DISABLED': 1 # By Default email verification is disabled, configure SMTP settings to enable(https://formbricks.com/docs/self-hosting/configuration/smtp) +- 'PASSWORD_RESET_DISABLED': 1 # By Default password reset is disabled, configure SMTP settings to enable(https://formbricks.com/docs/self-hosting/configuration/smtp) + +Retrieve them using: +```sh +kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.NEXTAUTH_SECRET}" | base64 --decode +kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.ENCRYPTION_KEY}" | base64 --decode +kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.CRON_SECRET}" | base64 --decode +``` + +--- + +Scaling: -5. Scaling: {{- if .Values.autoscaling.enabled }} - Horizontal Pod Autoscaling is enabled. - - Minimum replicas: {{ .Values.autoscaling.minReplicas }} - - Maximum replicas: {{ .Values.autoscaling.maxReplicas }} - - Target CPU utilization: {{ .Values.autoscaling.metrics.averageUtilization }}% - - To check the current status of the HPA, run: - kubectl get hpa -n {{ .Release.Namespace }} {{ include "formbricks.fullname" . }} + Horizontal Pod Autoscaling (HPA) is enabled. + - **Min Replicas**: `{{ .Values.autoscaling.minReplicas }}` + - **Max Replicas**: `{{ .Values.autoscaling.maxReplicas }}` + + Check HPA status: + ```sh + kubectl get hpa -n {{ .Release.Namespace }} {{ include "formbricks.name" . }} + ``` {{- else }} - Horizontal Pod Autoscaling is not enabled. Your deployment has a fixed number of {{ .Values.replicaCount }} replicas. - To scale manually, use: - kubectl scale deployment -n {{ .Release.Namespace }} {{ include "formbricks.fullname" . }} --replicas= + HPA is **not enabled**. Your deployment has a fixed number of `{{ .Values.replicaCount }}` replicas. + Manually scale using: + ```sh + kubectl scale deployment -n {{ .Release.Namespace }} {{ include "formbricks.name" . }} --replicas= + ``` {{- end }} -6. Persistence: - - PostgreSQL data is persisted with a {{ .Values.postgresql.primary.persistence.size }} storage. +--- + +External Secrets: +{{- if .Values.externalSecret.enabled }} + External secrets are enabled. + Ensure that your `SecretStore` or `ClusterSecretStore` is configured properly. + - Secret Store Name: `{{ .Values.externalSecret.secretStore.name }}` + - Refresh Interval: `{{ .Values.externalSecret.refreshInterval }}` + + Verify the external secret: + ```sh + kubectl get externalsecrets -n {{ .Release.Namespace }} + ``` +{{- else }} + External secrets are **not enabled**. +{{- end }} + +--- + +Persistence: + +{{- if .Values.postgresql.enabled }} + PostgreSQL data is persisted with `{{ .Values.postgresql.primary.persistence.size }}` storage. +{{- end }} {{- if .Values.redis.enabled }} - - Redis data is not persisted (persistence is disabled). + Redis data is persisted with `{{ .Values.redis.master.persistence.size }}` storage.. {{- end }} -7. Traefik Configuration: -{{- if .Values.traefik.enabled }} - Traefik is configured with the following settings: - - TLS enabled with Let's Encrypt - - HTTP to HTTPS redirect enabled - - ACME challenge type: HTTP - - Entrypoints: web (80) and websecure (443) -{{- else }} - Traefik is not enabled in your current configuration. -{{- end }} +--- -8. Formbricks Documentation and Support: - For more information, advanced configuration options, and support, please visit: - - Official Formbricks website: https://formbricks.com - - Documentation: https://formbricks.com/docs +Formbricks Documentation and Support: -If you encounter any issues or have questions, please refer to the Formbricks documentation -or reach out to the Formbricks community for support. \ No newline at end of file +For more information, advanced configurations, and support, visit: +- **Official Website**: https://formbricks.com +- **Documentation**: https://formbricks.com/docs + +For troubleshooting, refer to the documentation or community support. + +--- diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl index 86b7cbabac..bcf06347cc 100644 --- a/helm-chart/templates/_helpers.tpl +++ b/helm-chart/templates/_helpers.tpl @@ -1,51 +1,141 @@ {{/* Expand the name of the chart. +This function ensures that the chart name is either taken from `nameOverride` or defaults to `.Chart.Name`. +It also truncates the name to a maximum of 63 characters and removes trailing hyphens. */}} {{- define "formbricks.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "formbricks.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} {{/* -Create chart name and version as used by the chart label. +Define the application version to be used in labels. +The version is taken from `.Values.deployment.image.tag` if provided, otherwise it defaults to `.Chart.Version`. +It ensures the version only contains alphanumeric characters, underscores, dots, or hyphens, replacing any invalid characters with a hyphen. +*/}} +{{- define "formbricks.version" -}} + {{- $appVersion := default .Chart.Version .Values.deployment.image.tag -}} + {{- regexReplaceAll "[^a-zA-Z0-9_\\.\\-]" $appVersion "-" | trunc 63 | trimSuffix "-" -}} +{{- end }} + + +{{/* +Generate a chart name and version string to be used in Helm chart labels. +This follows the format: `-`, replacing `+` with `_` and truncating to 63 characters. */}} {{- define "formbricks.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} + {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} + {{/* -Common labels +Common labels applied to Kubernetes resources. +These labels help identify and manage the application. */}} {{- define "formbricks.labels" -}} helm.sh/chart: {{ include "formbricks.chart" . }} + +# Selector labels {{ include "formbricks.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} + +# Application version label +{{- with include "formbricks.version" . }} +app.kubernetes.io/version: {{ . | quote }} {{- end }} +# Managed by Helm +app.kubernetes.io/managed-by: {{ .Release.Service }} + +# Part of label, defaults to the chart name if `partOfOverride` is not provided. +app.kubernetes.io/part-of: {{ .Values.partOfOverride | default (include "formbricks.name" .) }} +{{- end }} + + {{/* -Selector labels +Selector labels used for identifying workloads in Kubernetes. +These labels ensure that selectors correctly map to the deployed resources. */}} {{- define "formbricks.selectorLabels" -}} app.kubernetes.io/name: {{ include "formbricks.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: {{ .Values.componentOverride | default (include "formbricks.name" .) }} +{{- end }} + + +{{/* +Renders a value that contains a Helm template. +Usage: +{{ include "formbricks.tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $) }} +This function allows rendering values dynamically. +*/}} +{{- define "formbricks.tplvalues.render" -}} + {{- if typeIs "string" .value }} + {{- tpl .value .context }} + {{- else }} + {{- tpl (.value | toYaml) .context }} + {{- end }} +{{- end }} + + +{{/* +Allow the release namespace to be overridden. +If `namespaceOverride` is provided, it will be used; otherwise, it defaults to `.Release.Namespace`. +*/}} +{{- define "formbricks.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride -}} +{{- end -}} + + +{{- define "formbricks.postgresAdminPassword" -}} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }} +{{- if and $secret (index $secret.data "POSTGRES_ADMIN_PASSWORD") }} + {{- index $secret.data "POSTGRES_ADMIN_PASSWORD" | b64dec -}} +{{- else }} + {{- randAlphaNum 16 -}} +{{- end -}} +{{- end }} + +{{- define "formbricks.postgresUserPassword" -}} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }} +{{- if and $secret (index $secret.data "POSTGRES_USER_PASSWORD") }} + {{- index $secret.data "POSTGRES_USER_PASSWORD" | b64dec -}} +{{- else }} + {{- randAlphaNum 16 -}} +{{- end -}} +{{- end }} + +{{- define "formbricks.redisPassword" -}} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }} +{{- if and $secret (index $secret.data "REDIS_PASSWORD") }} + {{- index $secret.data "REDIS_PASSWORD" | b64dec -}} +{{- else }} + {{- randAlphaNum 16 -}} +{{- end -}} +{{- end }} + +{{- define "formbricks.cronSecret" -}} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }} +{{- if $secret }} + {{- index $secret.data "CRON_SECRET" | b64dec -}} +{{- else }} + {{- randAlphaNum 32 -}} +{{- end -}} +{{- end }} + +{{- define "formbricks.encryptionKey" -}} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }} +{{- if $secret }} + {{- index $secret.data "ENCRYPTION_KEY" | b64dec -}} +{{- else }} + {{- randAlphaNum 32 -}} +{{- end -}} +{{- end }} + +{{- define "formbricks.nextAuthSecret" -}} +{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }} +{{- if $secret }} + {{- index $secret.data "NEXTAUTH_SECRET" | b64dec -}} +{{- else }} + {{- randAlphaNum 32 -}} +{{- end -}} {{- end }} diff --git a/helm-chart/templates/cronjob.yaml b/helm-chart/templates/cronjob.yaml new file mode 100644 index 0000000000..b6051079ea --- /dev/null +++ b/helm-chart/templates/cronjob.yaml @@ -0,0 +1,102 @@ +{{- if (.Values.cronJob).enabled }} +{{- range $name, $job := .Values.cronJob.jobs }} +--- +apiVersion: {{ if $.Capabilities.APIVersions.Has "batch/v1/CronJob" }}batch/v1{{ else }}batch/v1beta1{{ end }} +kind: CronJob +metadata: + name: {{ $name }} + labels: + # Standard labels for tracking CronJobs + {{- include "formbricks.labels" $ | nindent 4 }} + + # Additional labels if specified + {{- if $job.additionalLabels }} + {{- toYaml $job.additionalLabels | indent 4 }} + {{- end }} + + # Additional annotations if specified + {{- if $job.annotations }} + annotations: + {{- toYaml $job.annotations | indent 4 }} + {{- end }} + +spec: + # Define the execution schedule for the job + schedule: {{ $job.schedule | quote }} + + # Kubernetes 1.27+ supports time zones for CronJobs + {{- if ge (int $.Capabilities.KubeVersion.Minor) 27 }} + {{- if $job.timeZone }} + timeZone: {{ $job.timeZone }} + {{- end }} + {{- end }} + + # Define job retention policies + {{- if $job.successfulJobsHistoryLimit }} + successfulJobsHistoryLimit: {{ $job.successfulJobsHistoryLimit }} + {{- end }} + {{- if $job.failedJobsHistoryLimit }} + failedJobsHistoryLimit: {{ $job.failedJobsHistoryLimit }} + {{- end }} + + # Define concurrency policy + {{- if $job.concurrencyPolicy }} + concurrencyPolicy: {{ $job.concurrencyPolicy }} + {{- end }} + + jobTemplate: + spec: + {{- with $job.activeDeadlineSeconds }} + activeDeadlineSeconds: {{ . }} + {{- end }} + {{- if not (kindIs "invalid" $job.backoffLimit) }} + backoffLimit: {{ $job.backoffLimit }} + {{- end }} + template: + metadata: + labels: + {{- include "formbricks.labels" $ | nindent 12 }} + + # Additional pod-level labels + {{- with $job.additionalPodLabels }} + {{- toYaml . | nindent 12 }} + {{- end }} + + # Additional annotations + {{- with $job.additionalPodAnnotations }} + annotations: {{- toYaml . | nindent 12 }} + {{- end }} + + spec: + # Define the service account if RBAC is enabled + {{- if $.Values.rbac.enabled }} + serviceAccountName: {{ template "formbricks.name" $ }} + {{- end }} + + # Define the job container + containers: + - name: {{ $name }} + image: "{{ required "Image repository is undefined" $job.image.repository }}:{{ $job.image.tag | default "latest" }}" + imagePullPolicy: {{ $job.image.imagePullPolicy | default "IfNotPresent" }} + + # Environment variables from values + {{- with $job.env }} + env: + {{- range $key, $value := $job.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + + # Define command and arguments if specified + {{- with $job.command }} + command: {{- toYaml . | indent 14 }} + {{- end }} + + {{- with $job.args }} + args: {{- toYaml . | indent 14 }} + {{- end }} + + restartPolicy: {{ $job.restartPolicy | default "OnFailure" }} +{{- end }} +{{- end }} diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml index 814355c584..e048576bad 100644 --- a/helm-chart/templates/deployment.yaml +++ b/helm-chart/templates/deployment.yaml @@ -1,34 +1,182 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "formbricks.fullname" . }} + name: {{ include "formbricks.name" . }} labels: {{- include "formbricks.labels" . | nindent 4 }} + {{- if .Values.deployment.additionalLabels }} + {{- toYaml .Values.deployment.additionalLabels | nindent 4 }} + {{- end }} + {{- if or .Values.deployment.annotations .Values.deployment.reloadOnChange }} + annotations: + {{- if .Values.deployment.annotations }} + {{- toYaml .Values.deployment.annotations | nindent 4 }} + {{- end }} + {{- end }} spec: - replicas: {{ .Values.replicaCount }} + {{- if .Values.deployment.replicas }} + replicas: {{ .Values.deployment.replicas }} + {{- end }} selector: matchLabels: {{- include "formbricks.selectorLabels" . | nindent 6 }} + {{- if .Values.deployment.strategy }} + strategy: + {{- toYaml .Values.deployment.strategy | nindent 4 }} + {{- end }} + {{- if not (kindIs "invalid" .Values.deployment.revisionHistoryLimit) }} + revisionHistoryLimit: {{ .Values.deployment.revisionHistoryLimit }} + {{- end }} template: metadata: labels: {{- include "formbricks.selectorLabels" . | nindent 8 }} + {{- if .Values.deployment.additionalPodLabels }} + {{- toYaml .Values.deployment.additionalPodLabels | nindent 8 }} + {{- end }} + {{- if .Values.deployment.disableIstioInject }} + sidecar.istio.io/inject: "false" + {{- end }} + {{- if .Values.deployment.additionalPodAnnotations }} + annotations: + {{- toYaml .Values.deployment.additionalPodAnnotations | nindent 8 }} + {{- end }} spec: + {{- if .Values.deployment.nodeSelector }} + nodeSelector: + {{- toYaml .Values.deployment.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.deployment.tolerations }} + tolerations: + {{- toYaml .Values.deployment.tolerations | nindent 8 }} + {{- end }} + {{- if .Values.deployment.affinity }} + affinity: + {{- toYaml .Values.deployment.affinity | nindent 8 }} + {{- end }} + {{- if .Values.deployment.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml .Values.deployment.topologySpreadConstraints | nindent 10 }} + {{- end }} + {{- if .Values.deployment.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.deployment.imagePullSecrets | nindent 8 }} + {{- end }} + {{- if .Values.deployment.hostNetwork }} + hostNetwork: true + {{- end }} + {{- if .Values.rbac.serviceAccount.enabled }} + serviceAccountName: {{ .Values.rbac.serviceAccount.name | default (include "formbricks.name" .) }} + {{- end }} + {{- if .Values.deployment.securityContext }} + securityContext: + {{ toYaml .Values.deployment.securityContext | indent 8 }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds | default 30 }} containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} + - name: {{ template "formbricks.name" . }} + image: "{{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.deployment.image.pullPolicy }} + {{- if .Values.deployment.command }} + command: + {{- toYaml .Values.deployment.command | nindent 12 }} + {{- end }} + {{- if .Values.deployment.args }} + args: + {{- toYaml .Values.deployment.args | nindent 12 }} + {{- end }} + {{- if .Values.deployment.ports }} ports: - - name: http - containerPort: 3000 - protocol: TCP - envFrom: - - secretRef: - name: {{ include "formbricks.fullname" . }}-secrets - env: - {{- range $key, $value := .Values.env }} - - name: {{ $key }} - value: {{ $value | quote }} + {{- range $name, $config := .Values.deployment.ports }} + - name: {{ $name | quote }} + containerPort: {{ $config.containerPort | default $config.port }} + protocol: {{ $config.protocol | default "TCP" | quote }} {{- end }} + {{- end }} + {{- if .Values.deployment.envFrom }} + envFrom: + {{- range $value := .Values.deployment.envFrom }} + {{- if (eq .type "configmap") }} + - configMapRef: + {{- if .name }} + name: {{ include "formbricks.tplvalues.render" ( dict "value" $value.name "context" $ ) }} + {{- else if .nameSuffix }} + name: {{ template "formbricks.name" $ }}-{{ include "formbricks.tplvalues.render" ( dict "value" $value.nameSuffix "context" $ ) }} + {{- else }} + name: {{ template "formbricks.name" $ }} + {{- end }} + {{- end }} + {{- if (eq .type "secret") }} + - secretRef: + {{- if .name }} + name: {{ include "formbricks.tplvalues.render" ( dict "value" $value.name "context" $ ) }} + {{- else if .nameSuffix }} + name: {{ template "formbricks.name" $ }}-{{ include "formbricks.tplvalues.render" ( dict "value" $value.nameSuffix "context" $ ) }} + {{- else }} + name: {{ template "formbricks.name" $ }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + env: + {{- if and (.Values.enterprise.enabled) (ne .Values.enterprise.licenseKey "") }} + - name: ENTERPRISE_LICENSE_KEY + valueFrom: + secretKeyRef: + name: {{ template "formbricks.name" . }}-app-secrets + key: ENTERPRISE_LICENSE_KEY + {{- else if and (.Values.enterprise.enabled) (eq .Values.enterprise.licenseKey "") }} + - name: ENTERPRISE_LICENSE_KEY + valueFrom: + secretKeyRef: + name: {{ template "formbricks.name" . }}-app-secrets + key: ENTERPRISE_LICENSE_KEY + {{- end }} + - name: REDIS_URL + valueFrom: + secretKeyRef: + name: {{ template "formbricks.name" . }}-app-secrets + key: REDIS_URL + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ template "formbricks.name" . }}-app-secrets + key: DATABASE_URL + - name: CRON_SECRET + valueFrom: + secretKeyRef: + name: {{ template "formbricks.name" . }}-app-secrets + key: CRON_SECRET + - name: ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ template "formbricks.name" . }}-app-secrets + key: ENCRYPTION_KEY + - name: NEXTAUTH_SECRET + valueFrom: + secretKeyRef: + name: {{ template "formbricks.name" . }}-app-secrets + key: NEXTAUTH_SECRET + {{- range $key, $value := .Values.deployment.env }} + - name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }} + {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | indent 10 }} + {{- end }} + {{- if .Values.deployment.resources }} resources: - {{- toYaml .Values.resources | nindent 12 }} \ No newline at end of file + {{- toYaml .Values.deployment.resources | nindent 12 }} + {{- end }} + {{- with .Values.deployment.probes }} + {{- if .livenessProbe }} + livenessProbe: + {{- toYaml .livenessProbe | nindent 12 }} + {{- end }} + {{- if .readinessProbe }} + readinessProbe: + {{- toYaml .readinessProbe | nindent 12 }} + {{- end }} + {{- if .startupProbe }} + startupProbe: + {{- toYaml .startupProbe | nindent 12 }} + {{- end }} + {{- end }} diff --git a/helm-chart/templates/externalsecrets.yaml b/helm-chart/templates/externalsecrets.yaml new file mode 100644 index 0000000000..4acbddde81 --- /dev/null +++ b/helm-chart/templates/externalsecrets.yaml @@ -0,0 +1,52 @@ +{{- if (.Values.externalSecret).enabled }} +{{- range $nameSuffix, $data := .Values.externalSecret.files }} +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: {{ template "formbricks.name" $ }}-{{ $nameSuffix }} + labels: + {{- include "formbricks.labels" $ | nindent 4 }} +spec: + refreshInterval: {{ $.Values.externalSecret.refreshInterval }} +{{- if and (not $data.data) (not $data.dataFrom) }} +{{- fail "Data or datafrom not specified for secret {{ template 'formbricks.name' $ }}-{{ $nameSuffix }} " }} +{{- end }} +{{- if $data.data }} + data: +{{- range $secretKey, $remoteRef := $data.data}} + - secretKey: {{ $secretKey }} +{{ toYaml $remoteRef | indent 6}} +{{- end }} +{{- end }} +{{- if $data.dataFrom }} + dataFrom: + - extract: +{{ toYaml $data.dataFrom | indent 6 }} +{{- end }} + {{- if $data.secretStore }} + secretStoreRef: + name: {{ $data.secretStore.name }} + kind: {{ $data.secretStore.kind | default "SecretStore" }} + {{- else }} + secretStoreRef: + name: {{ $.Values.externalSecret.secretStore.name }} + kind: {{ $.Values.externalSecret.secretStore.kind | default "SecretStore" }} + {{- end}} + target: + name: {{ template "formbricks.name" $ }}-{{ $nameSuffix }} + template: + type: {{ $data.type | default "Opaque" }} +{{- if or $data.annotations $data.labels}} + metadata: +{{- if $data.annotations }} + annotations: +{{ toYaml $data.annotations | indent 10 }} +{{- end }} +{{- if $data.labels }} + labels: +{{ toYaml $data.labels | indent 10 }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/helm-chart/templates/hpa.yaml b/helm-chart/templates/hpa.yaml index fe4d538f7e..5adb814124 100644 --- a/helm-chart/templates/hpa.yaml +++ b/helm-chart/templates/hpa.yaml @@ -1,39 +1,33 @@ {{- if .Values.autoscaling.enabled }} +--- +{{- if .Capabilities.APIVersions.Has "autoscaling/v2/HorizontalPodAutoscaler" }} apiVersion: autoscaling/v2 +{{- else }} +apiVersion: autoscaling/v2beta2 +{{- end }} kind: HorizontalPodAutoscaler metadata: - name: {{ include "formbricks.fullname" . }} + name: {{ template "formbricks.name" . }} + labels: + {{- include "formbricks.labels" . | nindent 4 }} + {{- with .Values.autoscaling.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if .Values.autoscaling.annotations }} + annotations: + {{- toYaml .Values.autoscaling.annotations | nindent 4 }} + {{- end }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment - name: {{ include "formbricks.fullname" . }} + name: {{ template "formbricks.name" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} + {{- toYaml .Values.autoscaling.metrics | nindent 4 }} + {{- if .Values.autoscaling.behavior }} behavior: - scaleDown: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - scaleUp: - stabilizationWindowSeconds: 0 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - - type: Pods - value: 4 - periodSeconds: 15 - selectPolicy: Max -{{- end }} \ No newline at end of file + {{- toYaml .Values.autoscaling.behavior | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/ingress.yaml b/helm-chart/templates/ingress.yaml index 3139ace968..782d5dcc89 100644 --- a/helm-chart/templates/ingress.yaml +++ b/helm-chart/templates/ingress.yaml @@ -1,23 +1,45 @@ -{{- if .Values.traefik.enabled -}} -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute +{{- if (.Values.ingress).enabled -}} +{{- $applicationNameTpl := include "formbricks.name" . -}} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress metadata: - name: {{ include "formbricks.fullname" . }} + name: {{ template "formbricks.name" . }} + namespace: {{ include "formbricks.namespace" . }} labels: - {{- include "formbricks.labels" . | nindent 4 }} - {{- with .Values.traefik.ingressRoute.annotations }} + {{- include "formbricks.labels" $ | nindent 4 }} +{{- if .Values.ingress.additionalLabels }} +{{ toYaml .Values.ingress.additionalLabels | indent 4 }} +{{- end }} +{{- if .Values.ingress.annotations }} annotations: - {{- toYaml . | nindent 4 }} - {{- end }} +{{ toYaml .Values.ingress.annotations | indent 4 }} +{{- end }} spec: - entryPoints: - - websecure - routes: - - match: Host(`{{ .Values.hostname }}`) - kind: Rule - services: - - name: {{ include "formbricks.fullname" . }} - port: {{ .Values.service.port }} +{{- if .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName }} +{{- end}} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ tpl .host $ }} + http: + paths: + {{- if .paths }} + {{- range .paths }} + - path: {{ .path }} + pathType: {{ default "ImplementationSpecific" (.pathType) }} + backend: + service: + name: {{ default $applicationNameTpl (.serviceName) }} + port: + name: {{ default "http" (.servicePort) }} + {{- end }} + {{- else }} + {{ fail "Specify paths for ingress host, check values.yaml" }} + {{- end }} + {{- end -}} + {{- if .Values.ingress.tls }} tls: - certResolver: letsencrypt -{{- end }} \ No newline at end of file +{{ include "formbricks.tplvalues.render" (dict "value" .Values.ingress.tls "context" $) | indent 3 }} + {{- end -}} +{{- end -}} diff --git a/helm-chart/templates/secrets.yaml b/helm-chart/templates/secrets.yaml index b392af0205..29e1d1c2e8 100644 --- a/helm-chart/templates/secrets.yaml +++ b/helm-chart/templates/secrets.yaml @@ -1,19 +1,37 @@ +{{- if and (.Values.secret) (.Values.secret.enabled) }} + +{{- $postgresAdminPassword := include "formbricks.postgresAdminPassword" . }} +{{- $postgresUserPassword := include "formbricks.postgresUserPassword" . }} +{{- $redisPassword := include "formbricks.redisPassword" . }} +--- apiVersion: v1 kind: Secret metadata: - name: {{ include "formbricks.fullname" . }}-secrets -type: Opaque -stringData: - {{- if .Values.postgresql.externalUrl }} - DATABASE_URL: {{ .Values.postgresql.externalUrl }} - {{- else if .Values.postgresql.enabled }} - DATABASE_URL: postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ .Release.Name }}-postgresql:5432/{{ .Values.postgresql.auth.database }} + name: {{ template "formbricks.name" . }}-app-secrets + labels: + {{- include "formbricks.labels" . | nindent 4 }} +data: + {{- if .Values.redis.enabled }} + REDIS_URL: {{ printf "redis://:%s@formbricks-redis-master:6379" $redisPassword | b64enc }} + {{- else }} + REDIS_URL: {{ .Values.redis.externalRedisUrl | b64enc }} {{- end }} - {{- if .Values.redis.externalUrl }} - REDIS_URL: {{ .Values.redis.externalUrl }} - {{- else if .Values.redis.enabled }} - REDIS_URL: redis://:{{ .Values.redis.auth.password }}@{{ .Release.Name }}-redis-master:6379 + {{- if .Values.postgresql.enabled }} + DATABASE_URL: {{ printf "postgresql://formbricks:%s@formbricks-postgresql/formbricks" $postgresUserPassword | b64enc }} + {{- else }} + DATABASE_URL: {{ .Values.postgresql.externalDatabaseUrl | b64enc }} {{- end }} - NEXTAUTH_SECRET: {{ .Values.formbricksConfig.nextAuthSecret | default (randAlphaNum 32) | quote }} - ENCRYPTION_KEY: {{ .Values.formbricksConfig.encryptionKey | default (randAlphaNum 32) | quote }} - CRON_SECRET: {{ .Values.formbricksConfig.cronSecret | default (randAlphaNum 32) | quote }} \ No newline at end of file + CRON_SECRET: {{ include "formbricks.cronSecret" . | b64enc }} + ENCRYPTION_KEY: {{ include "formbricks.encryptionKey" . | b64enc }} + NEXTAUTH_SECRET: {{ include "formbricks.nextAuthSecret" . | b64enc }} + {{- if and (.Values.enterprise.licenseKey) (ne .Values.enterprise.licenseKey "") }} + ENTERPRISE_LICENSE_KEY: {{ .Values.enterprise.licenseKey | b64enc }} + {{- end }} + {{- if .Values.redis.enabled }} + REDIS_PASSWORD: {{ $redisPassword | b64enc }} + {{- end }} + {{- if .Values.postgresql.enabled }} + POSTGRES_ADMIN_PASSWORD: {{ $postgresAdminPassword | b64enc }} + POSTGRES_USER_PASSWORD: {{ $postgresUserPassword | b64enc }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/service.yaml b/helm-chart/templates/service.yaml index 27cb222780..7050ba7094 100644 --- a/helm-chart/templates/service.yaml +++ b/helm-chart/templates/service.yaml @@ -1,15 +1,55 @@ +{{- if (.Values.service).enabled }} +--- apiVersion: v1 kind: Service metadata: - name: {{ include "formbricks.fullname" . }} + name: {{ template "formbricks.name" . }} labels: + # Standard labels for tracking the service {{- include "formbricks.labels" . | nindent 4 }} + + # Additional labels from values + {{- if .Values.service.additionalLabels }} + {{- toYaml .Values.service.additionalLabels | nindent 4 }} + {{- end }} + + # Annotations for service configuration + {{- if .Values.service.annotations }} + annotations: + {{- include "formbricks.tplvalues.render" ( dict "value" .Values.service.annotations "context" $ ) | nindent 4 }} + {{- end }} + spec: + # Define the service type (ClusterIP, NodePort, LoadBalancer) type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: {{ .Values.service.targetPort | default 3000 }} - protocol: TCP - name: http + + # Define how the service selects pods selector: - {{- include "formbricks.selectorLabels" . | nindent 4 }} \ No newline at end of file + {{- include "formbricks.selectorLabels" . | nindent 4 }} + + # Include additional pod labels if defined + {{- if .Values.deployment.podLabels }} + {{- toYaml .Values.deployment.podLabels | nindent 4 }} + {{- end }} + + # Define the exposed service ports + ports: + {{- range $name, $config := .Values.deployment.ports }} + {{- if $config.exposed }} + - name: {{ $name }} + protocol: {{ default "TCP" $config.protocol | quote }} + port: {{ default $config.port $config.containerPort }} + targetPort: {{ default $config.port $config.containerPort }} + + # Define NodePort if service type is NodePort + {{- if $config.nodePort }} + nodePort: {{ $config.nodePort }} + {{- end }} + {{- end }} + {{- end }} + + # Include additional manually defined service ports + {{- if .Values.service.ports }} + {{- toYaml .Values.service.ports | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/serviceaccount.yaml b/helm-chart/templates/serviceaccount.yaml new file mode 100644 index 0000000000..ccc64b0ad9 --- /dev/null +++ b/helm-chart/templates/serviceaccount.yaml @@ -0,0 +1,22 @@ +{{- if and .Values.rbac.enabled .Values.rbac.serviceAccount.enabled }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + # Define the ServiceAccount name, either from values or generated + name: {{ default (include "formbricks.name" .) .Values.rbac.serviceAccount.name }} + labels: + # Standard labels for tracking the service account + {{- include "formbricks.labels" . | nindent 4 }} + + # Additional labels if provided + {{- with .Values.rbac.serviceAccount.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + + # Optional annotations for the service account + annotations: + {{- with .Values.rbac.serviceAccount.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm-chart/templates/tests/test-connection.yaml b/helm-chart/templates/tests/test-connection.yaml deleted file mode 100644 index 23cc03dee2..0000000000 --- a/helm-chart/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "formbricks.fullname" . }}-test-connection" - labels: - {{- include "formbricks.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "formbricks.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/helm-chart/templates/traefik-configmap.yaml b/helm-chart/templates/traefik-configmap.yaml deleted file mode 100644 index 74a6ad60eb..0000000000 --- a/helm-chart/templates/traefik-configmap.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: traefik-config -data: - traefik.toml: | - [certificatesResolvers.letsencrypt.acme] - email = {{ .Values.email }} \ No newline at end of file diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 1e3b44f0ec..379cad50f4 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -1,148 +1,293 @@ -image: - repository: ghcr.io/formbricks/formbricks - pullPolicy: IfNotPresent - tag: v3.2.0 +# Override the name for the application. +# If not set, the chart name will be used. +nameOverride: "" -service: - type: ClusterIP - port: 80 - targetPort: 3000 +# Override the name of the application component. +# Defaults to the same value as nameOverride. +componentOverride: "" -resources: - limits: - cpu: 500m - memory: 1Gi +# Override the application "part-of" label. +# Defaults to the chart name if not set. +partOfOverride: "" -autoscaling: +########################################################## +# Enterprise Configuration +########################################################## +enterprise: enabled: false - minReplicas: 2 - maxReplicas: 5 + licenseKey: "" + +########################################################## +# Deployment Configuration +########################################################## +deployment: + # Deployment strategy configuration + strategy: + type: RollingUpdate # Type of deployment strategy (RollingUpdate/Recreate) + # rollingUpdate: + # maxSurge: 25% + # maxUnavailable: 25% + + # Automatically reload deployment when ConfigMaps or Secrets change + reloadOnChange: false + + # NodeSelector for scheduling pods on specific nodes + nodeSelector: {} + + # Additional labels for Deployment + additionalLabels: {} + + # Additional pod labels to be used in the Service's label selector + additionalPodLabels: {} + + # Deployment annotations + annotations: {} + + # Additional pod annotations + additionalPodAnnotations: {} + + # Number of replicas + replicas: 1 + + # Image pull secrets for private container registries + imagePullSecrets: "" + + # Environment variables from ConfigMaps or Secrets + envFrom: + # app-secrets: + # type: secret + # nameSuffix: app-secrets + + # Environment variables passed to the app container + env: + EMAIL_VERIFICATION_DISABLED: + value: "1" + PASSWORD_RESET_DISABLED: + value: "1" + + # Tolerations for scheduling pods on tainted nodes + tolerations: [] + + # Pod affinity and anti-affinity rules + affinity: {} + + # Topology spread constraints for better scheduling + topologySpreadConstraints: [] + + # Number of previous ReplicaSet versions to retain + revisionHistoryLimit: 2 + + # Application container image + image: + repository: "ghcr.io/formbricks/formbricks" + digest: "" # If set, digest takes precedence over the tag + pullPolicy: IfNotPresent + + # Health probes configuration + probes: + startupProbe: + failureThreshold: 30 + periodSeconds: 10 + tcpSocket: + port: 3000 + + readinessProbe: + failureThreshold: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + initialDelaySeconds: 10 + httpGet: + path: /health + port: 3000 + + livenessProbe: + failureThreshold: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + initialDelaySeconds: 10 + httpGet: + path: /health + port: 3000 + + # Resource requests and limits + resources: + limits: + memory: 2Gi + requests: + memory: 1Gi + cpu: "1" + + # Container security context + containerSecurityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + + # Pod security context + securityContext: {} + + # Command override + command: [] + + # Arguments override + args: [] + + # Container ports + ports: + http: + containerPort: 3000 + protocol: TCP + exposed: true + metrics: + containerPort: 9464 + protocol: TCP + exposed: true + +########################################################## +# Horizontal Pod Autoscaler (HPA) +########################################################## +autoscaling: + enabled: true # Enable/disable HPA + additionalLabels: {} # Additional labels for the HPA resource + annotations: {} # Annotations for HPA + minReplicas: 1 # Minimum number of replicas + maxReplicas: 10 # Maximum number of replicas metrics: - type: Resource resource: name: cpu target: type: Utilization - averageUtilization: 80 - behavior: - scaleDown: - stabilizationWindowSeconds: 300 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - scaleUp: - stabilizationWindowSeconds: 0 - policies: - - type: Percent - value: 100 - periodSeconds: 15 - - type: Pods - value: 4 - periodSeconds: 15 - selectPolicy: Max + averageUtilization: 60 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 60 -replicaCount: 1 +########################################################## +# Service Configuration +########################################################## +service: + enabled: true # Enable/disable Kubernetes Service + additionalLabels: {} # Additional labels for Service + annotations: {} # Annotations for Service + type: ClusterIP # Service type (ClusterIP, NodePort, LoadBalancer) + ports: [] # Additional ports -livenessProbe: - httpGet: - path: /health - port: 3000 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - successThreshold: 1 +########################################################## +# Role-Based Access Control (RBAC) +########################################################## +rbac: + enabled: false # Enable/disable RBAC + serviceAccount: + enabled: false # Enable/disable ServiceAccount + name: "" # Custom ServiceAccount name + additionalLabels: {} # Additional labels + annotations: {} # Annotations -formbricksConfig: - nextAuthSecret: "" - encryptionKey: "" - cronSecret: "" +########################################################## +# Cron Job Configuration +########################################################## +cronJob: + enabled: false # Enable/disable CronJobs + jobs: {} # Define cron jobs -env: {} - -hostname: "" -email: "" - -traefik: +########################################################## +# Kubernetes Secret Configuration (Quick Start) +########################################################## +secret: enabled: true - ingressRoute: - dashboard: - enabled: false - additionalArguments: - - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com" - - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true" - - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" - - "--entrypoints.web.address=:80" - - "--entrypoints.websecure.address=:443" - - "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json" - - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-v02.api.letsencrypt.org/directory" - - "--log.level=DEBUG" - - "--entrypoints.web.http.redirections.entryPoint.to=websecure" - - "--entrypoints.web.http.redirections.entryPoint.scheme=https" - - "--entrypoints.web.http.redirections.entryPoint.permanent=true" - - "--providers.file.filename=/config/traefik.toml" - volumes: - - name: traefik-config - mountPath: "/config" - type: configMap - tls: - enabled: true - certResolver: letsencrypt - ports: - web: - port: 80 - websecure: - port: 443 - tls: - enabled: true - certResolver: letsencrypt - persistence: - enabled: true - name: traefik-acme - accessMode: ReadWriteOnce - size: 128Mi - path: /data - podSecurityContext: - fsGroup: 0 - hostNetwork: true - securityContext: - capabilities: - drop: - - ALL - add: - - NET_ADMIN - - NET_BIND_SERVICE - - NET_BROADCAST - - NET_RAW - runAsUser: 0 - runAsGroup: 0 - runAsNonRoot: false - readOnlyRootFilesystem: true +########################################################## +# External Secrets Configuration +########################################################## +externalSecret: + enabled: false # Enable/disable ExternalSecrets + secretStore: + name: aws-secrets-manager # Secret store reference name + kind: ClusterSecretStore # Type of secret store + refreshInterval: "1h" # Frequency of secret sync + files: {} + +########################################################## +# Ingress Configuration +########################################################## +ingress: + enabled: false # Enable/disable Ingress + ingressClassName: alb # Specify the Ingress class + hosts: + - host: k8s.formbricks.com + paths: + - path: / + pathType: "Prefix" + serviceName: "formbricks" + annotations: {} # Ingress annotations + + +########################################################## +# Redis Configuration +########################################################## redis: - enabled: false - externalUrl: "" + enabled: true # Enable/disable Redis + externalRedisUrl: "" + fullnameOverride: "formbricks-redis" architecture: standalone auth: enabled: true - password: redispassword + existingSecret: "formbricks-app-secrets" + existingSecretPasswordKey: "REDIS_PASSWORD" + networkPolicy: + enabled: false master: persistence: - enabled: false - replica: - replicaCount: 0 + enabled: true -postgresql: +########################################################## +# Service Monitor to collect Prometheus metrices +########################################################## +serviceMonitor: enabled: true + + # Additional labels + additionalLabels: + # key: value + + # Additional annotations + annotations: + # key: value + + # List of the endpoints of service from which prometheus will scrape data + endpoints: + - interval: 5s + path: /metrics + port: metrics + +########################################################## +# PostgreSQL Configuration +########################################################## +postgresql: + enabled: true # Enable/disable PostgreSQL + externalDatabaseUrl: "" + global: + security: + allowInsecureImages: true + fullnameOverride: "formbricks-postgresql" image: repository: pgvector/pgvector tag: 0.8.0-pg17 auth: username: formbricks - password: formbrickspassword database: formbricks + existingSecret: "formbricks-app-secrets" + secretKeys: + adminPasswordKey: "POSTGRES_ADMIN_PASSWORD" + userPasswordKey: "POSTGRES_USER_PASSWORD" primary: + networkPolicy: + enabled: false persistence: enabled: true size: 10Gi diff --git a/infra/terraform/.terraform.lock.hcl b/infra/terraform/.terraform.lock.hcl new file mode 100644 index 0000000000..d735a80a5f --- /dev/null +++ b/infra/terraform/.terraform.lock.hcl @@ -0,0 +1,164 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.89.0" + constraints = ">= 5.46.0, >= 5.79.0, >= 5.83.0" + hashes = [ + "h1:rFvk42jJEKiSUhK1cbERfNgYm4mD+8tq0ZcxCwpXSJs=", + "zh:0e55784d6effc33b9098ffab7fb77a242e0223a59cdcf964caa0be94d14684af", + "zh:23c64f3eaeffcafb007c89db3dfca94c8adf06b120af55abddaca55a6c6c924c", + "zh:338f620133cb607ce980f1725a0a78f61cbd42f4c601808ec1ee01a6c16c9811", + "zh:6ab0499172f17484d7b39924cf06782789df1473d31ebae0c7f3294f6e7a1227", + "zh:6dcde3e29e538cdf80971cbdce3b285056fd0e31dd64b02d2dcdf4c02f21d0a9", + "zh:75c9b594d77c9125bfb1aaf3fbd77a49e392841d53029b5726eb71d64de1233e", + "zh:7b334c23091e7b4c142e378416586292197c40a31a5bdb3b29c4f9afddd286f0", + "zh:991bbba72e5eb6eb351f466d68080992f5b0495f862a6723f386d1b4c965aa7d", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9bd2f12eef4a5dceafc211ab3b9a63f0e3e224007a60c1bbb842f76e0377033d", + "zh:b1ac1eb3b3e1a79fa5e5ad3364615f23b9ee0b093ceeb809fd386a4d40e7abb4", + "zh:cea91f43151b30c428c441b97c3b98bf1e5fb72ef72f6971308e3895e23437f4", + "zh:d3f000a1696a43d8186a516aace7d476d1fd76443627980504133477e19c8ecb", + "zh:d6f526fbbb3e51b3acc3b9640a158f7acc4a089632fca8ec6db430b450673f25", + "zh:e0c542950f96c93e761d50602e449fef8447f1389a6d5242a0a7dc9b06826d0b", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.6" + constraints = ">= 2.0.0" + hashes = [ + "h1:afnqn3XPnO40laFt+SVHPPKsg1j3HXT0VAO0xBVvmrY=", + "zh:1321b5ddede56be3f9b35bf75d7cda79adcb357fad62eb8677b6595e0baaa6cd", + "zh:265d66e61b9cd16ca1182ebf094cc0a08fb3687e8193a1dbac6899b16c237151", + "zh:3875c3a20e082ac55d5ff24bcaf7133ebc90c7f999fd0fb37cf0f0003474c94c", + "zh:68ce41ccd07757c451682703840cae1ec270ed5275cd491bbf8279782dfcbb73", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8dca3bb3f85ff8ac4d1b3f93975dcb751ed788396c56ebf0c3737ce1a4c60492", + "zh:9339bdaa99939291cedf543861353c8e7171ec5231c0dfacaa9bdb3338978dab", + "zh:a8510c2256e9a78697910bb5542aeca457c81225ea88130335f6d14a36a36c74", + "zh:af7ed71b8fceb60a5e3b7fa663be171e0bd41bb0af30e0e1f06a004c7b584e4a", + "zh:bc9de0f921b69d07f5fc1ea65f8af71d8d1a7053aafb500788b30bfce64b8fbe", + "zh:bccd0a49f161a91660d7d30dd6b389e6820f29752ccf351f10a3297c96973823", + "zh:c69321caca20009abead617f888a67aca990276cb7388b738b19157b88749190", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.17" + hashes = [ + "h1:kQMkcPVvHOguOqnxoEU2sm1ND9vCHiT8TvZ2x6v/Rsw=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.36.0" + constraints = "~> 2.36" + hashes = [ + "h1:94wlXkBzfXwyLVuJVhMdzK+VGjFnMjdmFkYhQ1RUFhI=", + "zh:07f38fcb7578984a3e2c8cf0397c880f6b3eb2a722a120a08a634a607ea495ca", + "zh:1adde61769c50dbb799d8bf8bfd5c8c504a37017dfd06c7820f82bcf44ca0d39", + "zh:39707f23ab58fd0e686967c0f973c0f5a39c14d6ccfc757f97c345fdd0cd4624", + "zh:4cc3dc2b5d06cc22d1c734f7162b0a8fdc61990ff9efb64e59412d65a7ccc92a", + "zh:8382dcb82ba7303715b5e67939e07dd1c8ecddbe01d12f39b82b2b7d7357e1d9", + "zh:88e8e4f90034186b8bfdea1b8d394621cbc46a064ff2418027e6dba6807d5227", + "zh:a6276a75ad170f76d88263fdb5f9558998bf3a3f7650d7bd3387b396410e59f3", + "zh:bc816c7e0606e5df98a0c7634b240bb0c8100c3107b8b17b554af702edc6a0c5", + "zh:cb2f31d58f37020e840af52755c18afd1f09a833c4903ac59270ab440fab57b7", + "zh:ee0d103b8d0089fb1918311683110b4492a9346f0471b136af46d3b019576b22", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f688b9ec761721e401f6859c19c083e3be20a650426f4747cd359cdc079d212a", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.3" + constraints = ">= 3.0.0" + hashes = [ + "h1:I0Um8UkrMUb81Fxq/dxbr3HLP2cecTH2WMJiwKSrwQY=", + "zh:22d062e5278d872fe7aed834f5577ba0a5afe34a3bdac2b81f828d8d3e6706d2", + "zh:23dead00493ad863729495dc212fd6c29b8293e707b055ce5ba21ee453ce552d", + "zh:28299accf21763ca1ca144d8f660688d7c2ad0b105b7202554ca60b02a3856d3", + "zh:55c9e8a9ac25a7652df8c51a8a9a422bd67d784061b1de2dc9fe6c3cb4e77f2f", + "zh:756586535d11698a216291c06b9ed8a5cc6a4ec43eee1ee09ecd5c6a9e297ac1", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:9d5eea62fdb587eeb96a8c4d782459f4e6b73baeece4d04b4a40e44faaee9301", + "zh:a6355f596a3fb8fc85c2fb054ab14e722991533f87f928e7169a486462c74670", + "zh:b5a65a789cff4ada58a5baffc76cb9767dc26ec6b45c00d2ec8b1b027f6db4ed", + "zh:db5ab669cf11d0e9f81dc380a6fdfcac437aea3d69109c7aef1a5426639d2d65", + "zh:de655d251c470197bcbb5ac45d289595295acb8f829f6c781d4a75c8c8b7c7dd", + "zh:f5c68199f2e6076bce92a12230434782bf768103a427e9bb9abee99b116af7b5", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.1" + hashes = [ + "h1:t152MY0tQH4a8fLzTtEWx70ITd3azVOrFDn/pQblbto=", + "zh:3193b89b43bf5805493e290374cdda5132578de6535f8009547c8b5d7a351585", + "zh:3218320de4be943e5812ed3de995946056db86eb8d03aa3f074e0c7316599bef", + "zh:419861805a37fa443e7d63b69fb3279926ccf98a79d256c422d5d82f0f387d1d", + "zh:4df9bd9d839b8fc11a3b8098a604b9b46e2235eb65ef15f4432bde0e175f9ca6", + "zh:5814be3f9c9cc39d2955d6f083bae793050d75c572e70ca11ccceb5517ced6b1", + "zh:63c6548a06de1231c8ee5570e42ca09c4b3db336578ded39b938f2156f06dd2e", + "zh:697e434c6bdee0502cc3deb098263b8dcd63948e8a96d61722811628dce2eba1", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a0b8e44927e6327852bbfdc9d408d802569367f1e22a95bcdd7181b1c3b07601", + "zh:b7d3af018683ef22794eea9c218bc72d7c35a2b3ede9233b69653b3c782ee436", + "zh:d63b911d618a6fe446c65bfc21e793a7663e934b2fef833d42d3ccd38dd8d68d", + "zh:fa985cd0b11e6d651f47cff3055f0a9fd085ec190b6dbe99bf5448174434cdea", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.12.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:JzYsPugN8Fb7C4NlfLoFu7BBPuRVT2/fCOdCaxshveI=", + "zh:090023137df8effe8804e81c65f636dadf8f9d35b79c3afff282d39367ba44b2", + "zh:26f1e458358ba55f6558613f1427dcfa6ae2be5119b722d0b3adb27cd001efea", + "zh:272ccc73a03384b72b964918c7afeb22c2e6be22460d92b150aaf28f29a7d511", + "zh:438b8c74f5ed62fe921bd1078abe628a6675e44912933100ea4fa26863e340e9", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:85c8bd8eefc4afc33445de2ee7fbf33a7807bc34eb3734b8eefa4e98e4cddf38", + "zh:98bbe309c9ff5b2352de6a047e0ec6c7e3764b4ed3dfd370839c4be2fbfff869", + "zh:9c7bf8c56da1b124e0e2f3210a1915e778bab2be924481af684695b52672891e", + "zh:d2200f7f6ab8ecb8373cda796b864ad4867f5c255cff9d3b032f666e4c78f625", + "zh:d8c7926feaddfdc08d5ebb41b03445166df8c125417b28d64712dccd9feef136", + "zh:e2412a192fc340c61b373d6c20c9d805d7d3dee6c720c34db23c2a8ff0abd71b", + "zh:e6ac6bba391afe728a099df344dbd6481425b06d61697522017b8f7a59957d44", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.6" + constraints = ">= 3.0.0" + hashes = [ + "h1:n3M50qfWfRSpQV9Pwcvuse03pEizqrmYEryxKky4so4=", + "zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8", + "zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297", + "zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb", + "zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1", + "zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509", + "zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8", + "zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a", + "zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18", + "zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50", + "zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27", + "zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/infra/terraform/bootstrap.tf b/infra/terraform/bootstrap.tf new file mode 100644 index 0000000000..d0ebcb374a --- /dev/null +++ b/infra/terraform/bootstrap.tf @@ -0,0 +1,177 @@ +# ################################################################################ +# # GitOps Bridge: Bootstrap +# ################################################################################ +# locals { +# addons = { +# enable_cert_manager = true +# enable_external_dns = true +# enable_istio = false +# enable_istio_ingress = false +# enable_external_secrets = true +# enable_metrics_server = false +# enable_keda = false +# enable_aws_load_balancer_controller = true +# enable_aws_ebs_csi_resources = false +# enable_velero = false +# enable_observability = false +# enable_karpenter = true +# } +# +# addons_default_versions = { +# cert_manager = "v1.17.1" +# external_dns = "1.15.2" +# karpenter = "1.3.0" +# external_secrets = "0.14.3" +# aws_load_balancer_controller = "1.10.0" +# # keda = "2.16.0" +# # istio = "1.23.3" +# } +# +# addons_metadata = merge( +# # module.addons.gitops_metadata +# { +# aws_cluster_name = module.eks.cluster_name +# aws_region = data.aws_region.selected.name +# aws_account_id = data.aws_caller_identity.current.account_id +# aws_vpc_id = module.vpc.vpc_id +# } +# ) +# +# argocd_apps = { +# eks-addons = { +# project = "default" +# repo_url = var.addons_repo_url +# target_revision = var.addons_target_revision +# addons_repo_revision = var.addons_target_revision +# path = var.addons_repo_path +# values = merge({ +# addons_repo_revision = var.addons_target_revision +# certManager = { +# enabled = local.addons.enable_cert_manager +# iamRoleArn = try(module.addons.gitops_metadata.cert_manager_iam_role_arn, "") +# values = try(yamldecode(join("\n", var.cert_manager_helm_config.values)), {}) +# chartVersion = try(var.cert_manager_helm_config.chart_version, local.addons_default_versions.cert_manager) +# } +# externalDNS = { +# enabled = local.addons.enable_external_dns +# iamRoleArn = try(module.addons.gitops_metadata.external_dns_iam_role_arn, "") +# values = try(yamldecode(join("\n", var.external_dns_helm_config.values)), {}) +# chartVersion = try(var.external_dns_helm_config.chart_version, local.addons_default_versions.external_dns) +# } +# externalSecrets = { +# enabled = local.addons.enable_external_secrets +# iamRoleArn = try(module.addons.gitops_metadata.external_secrets_iam_role_arn, "") +# values = try(yamldecode(join("\n", var.external_secrets_helm_config.values)), {}) +# chartVersion = try(var.external_secrets_helm_config.chart_version, local.addons_default_versions.external_secrets) +# } +# karpenter = { +# enabled = true +# iamRoleArn = try(module.addons.gitops_metadata.karpenter_iam_role_arn, "") +# values = try(yamldecode(join("\n", var.karpenter_helm_config.values)), {}) +# chartVersion = try(var.karpenter_helm_config.chart_version, local.addons_default_versions.karpenter) +# enableCrdWebhookConfig = true +# clusterName = module.eks.cluster_name +# clusterEndpoint = module.eks.cluster_endpoint +# interruptionQueue = try(module.addons.gitops_metadata.karpenter_interruption_queue, null) +# nodeIamRoleName = try(module.addons.gitops_metadata.karpenter_node_iam_role_arn, null) +# } +# loadBalancerController = { +# enabled = local.addons.enable_aws_load_balancer_controller +# iamRoleArn = try(module.addons.gitops_metadata.aws_load_balancer_controller_iam_role_arn, "") +# values = try(yamldecode(join("\n", var.aws_load_balancer_controller_helm_config.values)), {}) +# clusterName = module.eks.cluster_name +# chartVersion = try(var.aws_load_balancer_controller_helm_config.chart_version, local.addons_default_versions.aws_load_balancer_controller) +# vpcId = module.vpc.vpc_id +# } +# }) +# } +# workloads = { +# project = "default" +# repo_url = var.workloads_repo_url +# target_revision = var.workloads_target_revision +# addons_repo_revision = var.workloads_target_revision +# path = var.workloads_repo_path +# values = merge({ +# addons_repo_revision = var.workloads_target_revision +# formbricks = { +# certificateArn = try(module.acm.acm_certificate_arn, "") +# ingressHost = "app.k8s.formbricks.com" +# env = { +# TEST = { +# value = "test " +# } +# } +# } +# }) +# } +# } +# } +# +# variable "enable_gitops_bridge_bootstrap" { +# default = true +# } +# +# module "gitops_bridge_bootstrap" { +# count = var.enable_gitops_bridge_bootstrap ? 1 : 0 +# source = "../modules/argocd-gitops-bridge" +# +# cluster = { +# metadata = local.addons_metadata +# } +# argocd = { +# chart_version = "7.8.7" +# values = [ +# <<-EOT +# global: +# nodeSelector: +# CriticalAddonsOnly: "true" +# tolerations: +# - key: "CriticalAddonsOnly" +# operator: "Exists" +# effect: "NoSchedule" +# configs: +# params: +# server.insecure: true +# EOT +# ] +# } +# apps = local.argocd_apps +# } +# +# ############################################################################### +# # EKS Blueprints Addons +# ############################################################################### +# module "addons" { +# source = "../modules/addons" +# oidc_provider_arn = module.eks.oidc_provider_arn +# aws_region = data.aws_region.selected.name +# aws_account_id = data.aws_caller_identity.current.account_id +# aws_partition = data.aws_partition.current.partition +# cluster_name = module.eks.cluster_name +# cluster_endpoint = module.eks.cluster_endpoint +# cluster_certificate_authority_data = module.eks.cluster_certificate_authority_data +# cluster_token = data.aws_eks_cluster_auth.eks.token +# cluster_version = module.eks.cluster_version +# vpc_id = module.vpc.vpc_id +# node_security_group_id = module.eks.node_security_group_id +# cluster_security_group_id = module.eks.cluster_security_group_id +# +# # Using GitOps Bridge +# create_kubernetes_resources = var.enable_gitops_bridge_bootstrap ? false : true +# +# # Cert Manager +# enable_cert_manager = local.addons.enable_cert_manager +# +# # External DNS +# enable_external_dns = local.addons.enable_external_dns +# +# # Karpenter +# enable_karpenter = local.addons.enable_karpenter +# +# # External Secrets +# enable_external_secrets = local.addons.enable_external_secrets +# +# # Load Balancer Controller +# enable_aws_load_balancer_controller = local.addons.enable_aws_load_balancer_controller +# +# } diff --git a/infra/terraform/data.tf b/infra/terraform/data.tf new file mode 100644 index 0000000000..fcf818d66e --- /dev/null +++ b/infra/terraform/data.tf @@ -0,0 +1,12 @@ +data "aws_region" "selected" {} +data "aws_caller_identity" "current" {} +data "aws_availability_zones" "available" {} +data "aws_partition" "current" {} + +data "aws_eks_cluster_auth" "eks" { + name = module.eks.cluster_name +} + +data "aws_ecrpublic_authorization_token" "token" { + provider = aws.virginia +} diff --git a/infra/terraform/iam.tf b/infra/terraform/iam.tf new file mode 100644 index 0000000000..f0af377861 --- /dev/null +++ b/infra/terraform/iam.tf @@ -0,0 +1,30 @@ +################################################################################ +# GitHub OIDC Provider +# Note: This is one per AWS account +################################################################################ +module "iam_github_oidc_provider" { + source = "terraform-aws-modules/iam/aws//modules/iam-github-oidc-provider" + version = "5.54.0" + + tags = local.tags +} + +################################################################################ +# GitHub OIDC Role +################################################################################ + +module "iam_github_oidc_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-github-oidc-role" + version = "5.54.0" + + name = "${local.name}-github" + + subjects = [ + "repo:formbricks/*:*", + ] + policies = { + Administrator = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" + } + + tags = local.tags +} diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf new file mode 100644 index 0000000000..c1327e5376 --- /dev/null +++ b/infra/terraform/main.tf @@ -0,0 +1,668 @@ +locals { + project = "formbricks" + environment = "prod" + name = "${local.project}-${local.environment}" + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + tags = { + Project = local.project + Environment = local.environment + MangedBy = "Terraform" + Blueprint = local.name + } + domain = "k8s.formbricks.com" + karpetner_helm_version = "1.3.1" + karpenter_namespace = "karpenter" +} + +################################################################################ +# Route53 Hosted Zone +################################################################################ +module "route53_zones" { + source = "terraform-aws-modules/route53/aws//modules/zones" + version = "4.1.0" + + zones = { + "k8s.formbricks.com" = { + comment = "${local.domain} (testing)" + tags = { + Name = local.domain + } + } + } +} + +output "route53_ns_records" { + value = module.route53_zones.route53_zone_name_servers +} + + +module "acm" { + source = "terraform-aws-modules/acm/aws" + version = "5.1.1" + + domain_name = local.domain + zone_id = module.route53_zones.route53_zone_zone_id[local.domain] + + subject_alternative_names = [ + "*.${local.domain}", + ] + + validation_method = "DNS" + + tags = local.tags +} + +################################################################################ +# VPC +################################################################################ +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "5.19.0" + + name = "${local.name}-vpc" + cidr = local.vpc_cidr + + azs = local.azs + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)] # /20 + public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)] # Public LB /24 + intra_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 52)] # eks interface /24 + database_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 56)] # RDS / Elastic cache /24 + database_subnet_group_name = "${local.name}-subnet-group" + + enable_nat_gateway = true + single_nat_gateway = true + + public_subnet_tags = { + "kubernetes.io/role/elb" = 1 + } + + private_subnet_tags = { + "kubernetes.io/role/internal-elb" = 1 + # Tags subnets for Karpenter auto-discovery + "karpenter.sh/discovery" = "${local.name}-eks" + } + + tags = local.tags +} + +################################################################################ +# VPC Endpoints Module +################################################################################ +module "vpc_vpc-endpoints" { + source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints" + version = "5.19.0" + + vpc_id = module.vpc.vpc_id + + endpoints = { + "s3" = { + service = "s3" + service_type = "Gateway" + route_table_ids = flatten([ + module.vpc.intra_route_table_ids, + module.vpc.private_route_table_ids, + module.vpc.public_route_table_ids + ]) + tags = { Name = "s3-vpc-endpoint" } + } + } + + tags = local.tags +} + +################################################################################ +# PostgreSQL Serverless v2 +################################################################################ +data "aws_rds_engine_version" "postgresql" { + engine = "aurora-postgresql" + version = "16.4" +} + +resource "random_password" "postgres" { + length = 20 + special = false +} + +module "rds-aurora" { + source = "terraform-aws-modules/rds-aurora/aws" + version = "9.12.0" + + name = "${local.name}-postgres" + engine = data.aws_rds_engine_version.postgresql.engine + engine_mode = "provisioned" + engine_version = data.aws_rds_engine_version.postgresql.version + storage_encrypted = true + master_username = "formbricks" + master_password = random_password.postgres.result + manage_master_user_password = false + + vpc_id = module.vpc.vpc_id + db_subnet_group_name = module.vpc.database_subnet_group_name + security_group_rules = { + vpc_ingress = { + cidr_blocks = module.vpc.private_subnets_cidr_blocks + } + } + performance_insights_enabled = true + + apply_immediately = true + skip_final_snapshot = true + + enable_http_endpoint = true + + serverlessv2_scaling_configuration = { + min_capacity = 0 + max_capacity = 10 + seconds_until_auto_pause = 3600 + } + + instance_class = "db.serverless" + + instances = { + one = {} + } + + tags = local.tags + +} + +################################################################################ +# ElastiCache Module +################################################################################ +resource "random_password" "valkey" { + length = 20 + special = false +} + +module "elasticache" { + source = "terraform-aws-modules/elasticache/aws" + version = "1.4.1" + + replication_group_id = "${local.name}-valkey" + + engine = "valkey" + engine_version = "7.2" + node_type = "cache.m7g.large" + + transit_encryption_enabled = true + auth_token = random_password.valkey.result + maintenance_window = "sun:05:00-sun:09:00" + apply_immediately = true + + # Security Group + vpc_id = module.vpc.vpc_id + security_group_rules = { + ingress_vpc = { + # Default type is `ingress` + # Default port is based on the default engine port + description = "VPC traffic" + cidr_ipv4 = module.vpc.vpc_cidr_block + } + } + + # Subnet Group + subnet_group_name = "${local.name}-valkey" + subnet_group_description = "${title(local.name)} subnet group" + subnet_ids = module.vpc.database_subnets + + # Parameter Group + create_parameter_group = true + parameter_group_name = "${local.name}-valkey" + parameter_group_family = "valkey7" + parameter_group_description = "${title(local.name)} parameter group" + parameters = [ + { + name = "latency-tracking" + value = "yes" + } + ] + + tags = local.tags +} + +################################################################################ +# EKS Module +################################################################################ +module "ebs_csi_driver_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "~> 5.52" + + role_name_prefix = "${local.name}-ebs-csi-driver-" + + attach_ebs_csi_policy = true + + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"] + } + } + + tags = local.tags +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "20.33.1" + + cluster_name = "${local.name}-eks" + cluster_version = "1.32" + + enable_cluster_creator_admin_permissions = true + cluster_endpoint_public_access = true + + cluster_addons = { + coredns = { + most_recent = true + } + eks-pod-identity-agent = { + most_recent = true + } + aws-ebs-csi-driver = { + most_recent = true + service_account_role_arn = module.ebs_csi_driver_irsa.iam_role_arn + } + kube-proxy = { + most_recent = true + } + vpc-cni = { + most_recent = true + } + } + + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + control_plane_subnet_ids = module.vpc.intra_subnets + + eks_managed_node_groups = { + system = { + ami_type = "BOTTLEROCKET_ARM_64" + instance_types = ["t4g.small"] + + min_size = 2 + max_size = 3 + desired_size = 2 + + labels = { + CriticalAddonsOnly = "true" + "karpenter.sh/controller" = "true" + } + + taints = { + addons = { + key = "CriticalAddonsOnly" + value = "true" + effect = "NO_SCHEDULE" + }, + } + } + } + + node_security_group_tags = merge(local.tags, { + # NOTE - if creating multiple security groups with this module, only tag the + # security group that Karpenter should utilize with the following tag + # (i.e. - at most, only one security group should have this tag in your account) + "karpenter.sh/discovery" = "${local.name}-eks" + }) + + tags = local.tags + +} + +module "karpenter" { + source = "terraform-aws-modules/eks/aws//modules/karpenter" + version = "20.34.0" + + cluster_name = module.eks.cluster_name + enable_v1_permissions = true + + # Name needs to match role name passed to the EC2NodeClass + node_iam_role_use_name_prefix = false + node_iam_role_name = local.name + create_pod_identity_association = true + namespace = local.karpenter_namespace + + # Used to attach additional IAM policies to the Karpenter node IAM role + node_iam_role_additional_policies = { + AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" + } + + tags = local.tags +} + +output "karpenter_node_role" { + value = module.karpenter.node_iam_role_name +} + + + +resource "helm_release" "karpenter_crds" { + name = "karpenter-crds" + repository = "oci://public.ecr.aws/karpenter" + repository_username = data.aws_ecrpublic_authorization_token.token.user_name + repository_password = data.aws_ecrpublic_authorization_token.token.password + chart = "karpenter-crd" + version = "1.3.1" + namespace = local.karpenter_namespace + values = [ + <<-EOT + webhook: + enabled: true + serviceNamespace: ${local.karpenter_namespace} + EOT + ] +} + +resource "helm_release" "karpenter" { + name = "karpenter" + repository = "oci://public.ecr.aws/karpenter" + repository_username = data.aws_ecrpublic_authorization_token.token.user_name + repository_password = data.aws_ecrpublic_authorization_token.token.password + chart = "karpenter" + version = "1.3.1" + namespace = local.karpenter_namespace + skip_crds = true + + values = [ + <<-EOT + nodeSelector: + karpenter.sh/controller: 'true' + dnsPolicy: Default + settings: + clusterName: ${module.eks.cluster_name} + clusterEndpoint: ${module.eks.cluster_endpoint} + interruptionQueue: ${module.karpenter.queue_name} + EOT + ] +} + +resource "kubernetes_manifest" "ec2_node_class" { + manifest = { + apiVersion = "karpenter.k8s.aws/v1" + kind = "EC2NodeClass" + metadata = { + name = "default" + } + spec = { + amiSelectorTerms = [ + { + alias = "bottlerocket@latest" + } + ] + role = module.karpenter.node_iam_role_name + subnetSelectorTerms = [ + { + tags = { + "karpenter.sh/discovery" = "${local.name}-eks" + } + } + ] + securityGroupSelectorTerms = [ + { + tags = { + "karpenter.sh/discovery" = "${local.name}-eks" + } + } + ] + tags = { + "karpenter.sh/discovery" = "${local.name}-eks" + } + } + } +} + +resource "kubernetes_manifest" "node_pool" { + manifest = { + apiVersion = "karpenter.sh/v1" + kind = "NodePool" + metadata = { + name = "default" + } + spec = { + template = { + spec = { + nodeClassRef = { + group = "karpenter.k8s.aws" + kind = "EC2NodeClass" + name = "default" + } + requirements = [ + { + key = "karpenter.k8s.aws/instance-family" + operator = "In" + values = ["c8g", "c7g", "m8g", "m7g", "r8g", "r7g"] + }, + { + key = "karpenter.k8s.aws/instance-cpu" + operator = "In" + values = ["2", "4", "8"] + }, + { + key = "karpenter.k8s.aws/instance-hypervisor" + operator = "In" + values = ["nitro"] + } + ] + } + } + limits = { + cpu = 100 + } + disruption = { + consolidationPolicy = "WhenEmpty" + consolidateAfter = "30s" + } + } + } +} + +module "eks_blueprints_addons" { + source = "aws-ia/eks-blueprints-addons/aws" + version = "~> 1" + + cluster_name = module.eks.cluster_name + cluster_endpoint = module.eks.cluster_endpoint + cluster_version = module.eks.cluster_version + oidc_provider_arn = module.eks.oidc_provider_arn + + enable_metrics_server = true + metrics_server = { + chart_version = "3.12.2" + } + + enable_aws_load_balancer_controller = true + aws_load_balancer_controller = { + chart_version = "1.10.0" + values = [ + <<-EOT + vpcId: ${module.vpc.vpc_id} + EOT + ] + } + enable_external_dns = true + external_dns_route53_zone_arns = [module.route53_zones.route53_zone_zone_arn[local.domain]] + external_dns = { + chart_version = "1.15.2" + } + enable_cert_manager = false + cert_manager = { + chart_version = "v1.17.1" + values = [ + <<-EOT + installCRDs: false + crds: + enabled: true + keep: true + EOT + ] + } + + enable_external_secrets = true + external_secrets = { + chart_version = "0.14.3" + } + + tags = local.tags +} + +### Formbricks App +module "s3-bucket" { + source = "terraform-aws-modules/s3-bucket/aws" + version = "4.6.0" + + bucket_prefix = "formbricks-" + force_destroy = true + control_object_ownership = true + object_ownership = "BucketOwnerPreferred" + +} + + +module "iam_policy" { + source = "terraform-aws-modules/iam/aws//modules/iam-policy" + version = "5.53.0" + + name_prefix = "formbricks-" + path = "/" + description = "Policy for fombricks app" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:*", + ] + Resource = [ + module.s3-bucket.s3_bucket_arn, + "${module.s3-bucket.s3_bucket_arn}/*" + ] + } + ] + }) +} + + +module "formkey-aws-access" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "5.53.0" + + role_name_prefix = "formbricks-" + + role_policy_arns = { + "formbricks" = module.iam_policy.arn + } + assume_role_condition_test = "StringLike" + + oidc_providers = { + eks = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["formbricks:*"] + } + } +} + + +resource "helm_release" "formbricks" { + name = "formbricks" + namespace = "formbricks" + chart = "${path.module}/../../helm-chart" + max_history = 5 + + values = [ + <<-EOT + postgresql: + enabled: false + redis: + enabled: false + ingress: + enabled: true + ingressClassName: alb + hosts: + - host: "app.${local.domain}" + paths: + - path: / + pathType: "Prefix" + serviceName: "formbricks" + annotations: + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/target-type: ip + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/certificate-arn: ${module.acm.acm_certificate_arn} + alb.ingress.kubernetes.io/healthcheck-path: "/health" + alb.ingress.kubernetes.io/group.name: formbricks + alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06" + secret: + enabled: false + rbac: + enabled: true + serviceAccount: + enabled: true + name: formbricks + annotations: + eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn} + serviceMonitor: + enabled: true + deployment: + image: + repository: "ghcr.io/formbricks/formbricks-experimental" + tag: "open-telemetry-for-prometheus" + pullPolicy: Always + env: + S3_BUCKET_NAME: + value: ${module.s3-bucket.s3_bucket_id} + RATE_LIMITING_DISABLED: + value: "1" + envFrom: + app-parameters: + type: secret + nameSuffix: {RELEASE.name}-app-parameters + annotations: + deployed_at: ${timestamp()} + externalSecret: + enabled: true # Enable/disable ExternalSecrets + secretStore: + name: aws-secrets-manager + kind: ClusterSecretStore + refreshInterval: "1h" + files: + app-parameters: + dataFrom: + key: "/prod/formbricks/env" + secretStore: + name: aws-parameter-store + kind: ClusterSecretStore + app-secrets: + data: + DATABASE_URL: + remoteRef: + key: "prod/formbricks/secrets" + property: DATABASE_URL + REDIS_URL: + remoteRef: + key: "prod/formbricks/secrets" + property: REDIS_URL + CRON_SECRET: + remoteRef: + key: "prod/formbricks/secrets" + property: CRON_SECRET + ENCRYPTION_KEY: + remoteRef: + key: "prod/formbricks/secrets" + property: ENCRYPTION_KEY + NEXTAUTH_SECRET: + remoteRef: + key: "prod/formbricks/secrets" + property: NEXTAUTH_SECRET + ENTERPRISE_LICENSE_KEY: + remoteRef: + key: "prod/formbricks/enterprise" + property: ENTERPRISE_LICENSE_KEY + EOT + ] +} + +# secrets password/keys diff --git a/infra/terraform/provider.tf b/infra/terraform/provider.tf new file mode 100644 index 0000000000..efa160e227 --- /dev/null +++ b/infra/terraform/provider.tf @@ -0,0 +1,31 @@ +provider "aws" { + region = "eu-central-1" +} + +provider "aws" { + region = "us-east-1" + alias = "virginia" +} + +terraform { + backend "s3" { + bucket = "715841356175-terraform" + key = "terraform.tfstate" + region = "eu-central-1" + dynamodb_table = "terraform-lock" + } +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + token = data.aws_eks_cluster_auth.eks.token +} + +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + token = data.aws_eks_cluster_auth.eks.token + } +} diff --git a/infra/terraform/secrets.tf b/infra/terraform/secrets.tf new file mode 100644 index 0000000000..ec98262af1 --- /dev/null +++ b/infra/terraform/secrets.tf @@ -0,0 +1,33 @@ +# Generate random secrets for formbricks +resource "random_password" "nextauth_secret" { + length = 32 + special = false +} + +resource "random_password" "encryption_key" { + length = 32 + special = false +} + +resource "random_password" "cron_secret" { + length = 32 + special = false +} + +# Create the first AWS Secrets Manager secret for environment variables +resource "aws_secretsmanager_secret" "formbricks_app_secrets" { + name = "prod/formbricks/secrets" +} + + + +resource "aws_secretsmanager_secret_version" "formbricks_app_secrets" { + secret_id = aws_secretsmanager_secret.formbricks_app_secrets.id + secret_string = jsonencode({ + NEXTAUTH_SECRET = random_password.nextauth_secret.result + ENCRYPTION_KEY = random_password.encryption_key.result + CRON_SECRET = random_password.cron_secret.result + DATABASE_URL = "postgres://formbricks:${random_password.postgres.result}@${module.rds-aurora.cluster_endpoint}/formbricks" + REDIS_URL = "rediss://:${random_password.valkey.result}@${module.elasticache.replication_group_primary_endpoint_address}:6379" + }) +} diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf new file mode 100644 index 0000000000..792d600548 --- /dev/null +++ b/infra/terraform/variables.tf @@ -0,0 +1 @@ +# diff --git a/infra/terraform/versions.tf b/infra/terraform/versions.tf new file mode 100644 index 0000000000..eceb7f8cf2 --- /dev/null +++ b/infra/terraform/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.46" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.36" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.17" + } + } +} From 0164eca206efab0104145555ee39423fef076ffa Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Thu, 13 Mar 2025 04:27:57 -0700 Subject: [PATCH 042/411] fix: survey display and card width (#4937) --- apps/web/modules/survey/link/components/legal-footer.tsx | 2 +- .../modules/survey/link/components/link-survey-wrapper.tsx | 2 +- apps/web/modules/ui/components/media-background/index.tsx | 2 +- apps/web/modules/ui/components/preview-survey/index.tsx | 2 +- .../surveys/src/components/questions/matrix-question.tsx | 6 ++++-- .../src/components/wrappers/scrollable-container.tsx | 4 ++-- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/web/modules/survey/link/components/legal-footer.tsx b/apps/web/modules/survey/link/components/legal-footer.tsx index f68bd811a8..2903799ae6 100644 --- a/apps/web/modules/survey/link/components/legal-footer.tsx +++ b/apps/web/modules/survey/link/components/legal-footer.tsx @@ -21,7 +21,7 @@ export const LegalFooter = ({ return (

-
+
{IMPRINT_URL && ( {t("common.imprint")} diff --git a/apps/web/modules/survey/link/components/link-survey-wrapper.tsx b/apps/web/modules/survey/link/components/link-survey-wrapper.tsx index 18acc02a39..f6cb4b464f 100644 --- a/apps/web/modules/survey/link/components/link-survey-wrapper.tsx +++ b/apps/web/modules/survey/link/components/link-survey-wrapper.tsx @@ -80,7 +80,7 @@ export const LinkSurveyWrapper = ({ onBackgroundLoaded={handleBackgroundLoaded}>
{!styling.isLogoHidden && project.logo?.url && } -
+
{isPreview && (
diff --git a/apps/web/modules/ui/components/media-background/index.tsx b/apps/web/modules/ui/components/media-background/index.tsx index 5c3956826c..803778b4af 100644 --- a/apps/web/modules/ui/components/media-background/index.tsx +++ b/apps/web/modules/ui/components/media-background/index.tsx @@ -166,7 +166,7 @@ export const MediaBackground: React.FC = ({ return (
+ className={`relative h-[90%] max-h-[42rem] w-[22rem] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}> {/* below element is use to create notch for the mobile device mockup */}
{surveyType === "link" && renderBackground()} diff --git a/apps/web/modules/ui/components/preview-survey/index.tsx b/apps/web/modules/ui/components/preview-survey/index.tsx index b6c5146962..96f0d7107a 100644 --- a/apps/web/modules/ui/components/preview-survey/index.tsx +++ b/apps/web/modules/ui/components/preview-survey/index.tsx @@ -392,7 +392,7 @@ export const PreviewSurvey = ({ )}
-
+
{questionRows.map((row, rowIndex) => ( - + {getLocalizedValue(row, languageCode)} diff --git a/packages/surveys/src/components/wrappers/scrollable-container.tsx b/packages/surveys/src/components/wrappers/scrollable-container.tsx index 769b3500ff..e0f6502ab5 100644 --- a/packages/surveys/src/components/wrappers/scrollable-container.tsx +++ b/packages/surveys/src/components/wrappers/scrollable-container.tsx @@ -48,9 +48,9 @@ export function ScrollableContainer({ children }: ScrollableContainerProps) { ref={containerRef} style={{ scrollbarGutter: "stable both-edges", - maxHeight: isSurveyPreview ? "40dvh" : "60dvh", + maxHeight: isSurveyPreview ? "42dvh" : "60dvh", }} - className={cn("fb-overflow-auto fb-px-4 fb-pb-6 fb-bg-survey-bg")}> + className={cn("fb-overflow-auto fb-px-4 fb-pb-4 fb-bg-survey-bg")}> {children}
{!isAtBottom && ( From 2d028d18e5e76c38f8ae64969bc0eaacc14fdbd5 Mon Sep 17 00:00:00 2001 From: IllimarR Date: Thu, 13 Mar 2025 14:50:15 +0200 Subject: [PATCH 043/411] feat: possibility to set mail from name (#4864) Co-authored-by: Piyush Gupta Co-authored-by: Johannes --- .env.example | 2 ++ apps/web/modules/email/index.tsx | 3 ++- docker/docker-compose.yml | 1 + docker/formbricks.sh | 5 +++++ docs/self-hosting/advanced/migration.mdx | 1 + docs/self-hosting/configuration/environment-variables.mdx | 1 + docs/self-hosting/configuration/smtp.mdx | 5 +++++ packages/lib/constants.ts | 1 + packages/lib/env.ts | 2 ++ turbo.json | 1 + 10 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index ce10efce27..6243260549 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,7 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu # See optional configurations below if you want to disable these features. MAIL_FROM=noreply@example.com +MAIL_FROM_NAME=Formbricks SMTP_HOST=localhost SMTP_PORT=1025 # Enable SMTP_SECURE_ENABLED for TLS (port 465) @@ -207,3 +208,4 @@ UNKEY_ROOT_KEY= # Enable Prometheus metrics # PROMETHEUS_ENABLED= # PROMETHEUS_EXPORTER_PORT= + diff --git a/apps/web/modules/email/index.tsx b/apps/web/modules/email/index.tsx index d7c1720ecf..b99074d768 100644 --- a/apps/web/modules/email/index.tsx +++ b/apps/web/modules/email/index.tsx @@ -6,6 +6,7 @@ import type SMTPTransport from "nodemailer/lib/smtp-transport"; import { DEBUG, MAIL_FROM, + MAIL_FROM_NAME, SMTP_AUTHENTICATED, SMTP_HOST, SMTP_PASSWORD, @@ -69,7 +70,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise } as SMTPTransport.Options); const emailDefaults = { - from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`, + from: `${MAIL_FROM_NAME ?? "Formbricks"} <${MAIL_FROM ?? "noreply@formbricks.com"}>`, }; await transporter.sendMail({ ...emailDefaults, ...emailData }); diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 97fa7baf1e..4e87afb250 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -36,6 +36,7 @@ x-environment: &environment # Email Configuration # MAIL_FROM: + # MAIL_FROM_NAME: # SMTP_HOST: # SMTP_PORT: # SMTP_USER: diff --git a/docker/formbricks.sh b/docker/formbricks.sh index d941c6b5af..95e039e6c3 100755 --- a/docker/formbricks.sh +++ b/docker/formbricks.sh @@ -224,6 +224,9 @@ EOT echo -n "Enter your SMTP configured Email ID: " read mail_from + echo -n "Enter your SMTP configured Email Name: " + read mail_from_name + echo -n "Enter your SMTP Host URL: " read smtp_host @@ -244,6 +247,7 @@ EOT else mail_from="" + mail_from_name="" smtp_host="" smtp_port="" smtp_user="" @@ -270,6 +274,7 @@ EOT if [[ -n $mail_from ]]; then sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml + sed -i "s|# MAIL_FROM_NAME:|MAIL_FROM_NAME: \"$mail_from_name\"|" docker-compose.yml sed -i "s|# SMTP_HOST:|SMTP_HOST: \"$smtp_host\"|" docker-compose.yml sed -i "s|# SMTP_PORT:|SMTP_PORT: \"$smtp_port\"|" docker-compose.yml sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml diff --git a/docs/self-hosting/advanced/migration.mdx b/docs/self-hosting/advanced/migration.mdx index e2a889169a..858c95a1cb 100644 --- a/docs/self-hosting/advanced/migration.mdx +++ b/docs/self-hosting/advanced/migration.mdx @@ -1039,6 +1039,7 @@ x-environment: &environment # Email Configuration MAIL_FROM: + MAIL_FROM_NAME: SMTP_HOST: SMTP_PORT: SMTP_SECURE_ENABLED: diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index 7281ebf19b..77f768ce22 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -33,6 +33,7 @@ These variables are present inside your machine’s docker-compose file. Restart | RATE_LIMITING_DISABLED | Disables rate limiting if set to 1. | optional | | | INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | | | MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | | +| MAIL_FROM_NAME | Email name/title to send emails from. | optional (required if email services are to be enabled) | | | SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | | | SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | | | SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | | diff --git a/docs/self-hosting/configuration/smtp.mdx b/docs/self-hosting/configuration/smtp.mdx index d739becec7..0c19fcf8a5 100644 --- a/docs/self-hosting/configuration/smtp.mdx +++ b/docs/self-hosting/configuration/smtp.mdx @@ -33,6 +33,7 @@ To enable email functionality, configure the following environment variables: ```bash # Basic SMTP Configuration MAIL_FROM=noreply@yourdomain.com +MAIL_FROM_NAME=Formbricks SMTP_HOST=smtp.yourprovider.com SMTP_PORT=587 SMTP_USER=your_username @@ -75,6 +76,7 @@ If you're using the one-click setup with Docker Compose, you can either: environment: # Email Configuration MAIL_FROM: noreply@yourdomain.com + MAIL_FROM_NAME: Formbricks SMTP_HOST: smtp.yourprovider.com SMTP_PORT: 587 SMTP_USER: your_username @@ -95,6 +97,7 @@ environment: ```bash MAIL_FROM=noreply@yourdomain.com +MAIL_FROM_NAME=Formbricks SMTP_HOST=smtp.sendgrid.net SMTP_PORT=587 SMTP_USER=apikey @@ -105,6 +108,7 @@ SMTP_PASSWORD=your_sendgrid_api_key ```bash MAIL_FROM=noreply@yourdomain.com +MAIL_FROM_NAME=Formbricks SMTP_HOST=email-smtp.us-east-1.amazonaws.com SMTP_PORT=587 SMTP_USER=your_ses_access_key @@ -115,6 +119,7 @@ SMTP_PASSWORD=your_ses_secret_key ```bash MAIL_FROM=your_email@gmail.com +MAIL_FROM_NAME=Formbricks SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USER=your_email@gmail.com diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index c936224efa..095ba2fe35 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -83,6 +83,7 @@ export const SMTP_PASSWORD = env.SMTP_PASSWORD; export const SMTP_AUTHENTICATED = env.SMTP_AUTHENTICATED !== "0"; export const SMTP_REJECT_UNAUTHORIZED_TLS = env.SMTP_REJECT_UNAUTHORIZED_TLS !== "0"; export const MAIL_FROM = env.MAIL_FROM; +export const MAIL_FROM_NAME = env.MAIL_FROM_NAME; export const NEXTAUTH_SECRET = env.NEXTAUTH_SECRET; export const ITEMS_PER_PAGE = 30; diff --git a/packages/lib/env.ts b/packages/lib/env.ts index 95267506dd..c2b13e147f 100644 --- a/packages/lib/env.ts +++ b/packages/lib/env.ts @@ -49,6 +49,7 @@ export const env = createEnv({ INTERCOM_SECRET_KEY: z.string().optional(), IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(), MAIL_FROM: z.string().email().optional(), + MAIL_FROM_NAME: z.string().optional(), NEXTAUTH_SECRET: z.string().min(1), NOTION_OAUTH_CLIENT_ID: z.string().optional(), NOTION_OAUTH_CLIENT_SECRET: z.string().optional(), @@ -173,6 +174,7 @@ export const env = createEnv({ INTERCOM_SECRET_KEY: process.env.INTERCOM_SECRET_KEY, IS_FORMBRICKS_CLOUD: process.env.IS_FORMBRICKS_CLOUD, MAIL_FROM: process.env.MAIL_FROM, + MAIL_FROM_NAME: process.env.MAIL_FROM_NAME, NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, NEXT_PUBLIC_FORMBRICKS_API_HOST: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST, NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID, diff --git a/turbo.json b/turbo.json index 8235a1c082..c8ac3cc8fe 100644 --- a/turbo.json +++ b/turbo.json @@ -119,6 +119,7 @@ "IS_FORMBRICKS_CLOUD", "INTERCOM_SECRET_KEY", "MAIL_FROM", + "MAIL_FROM_NAME", "NEXT_PUBLIC_LAYER_API_KEY", "NEXT_PUBLIC_DOCSEARCH_APP_ID", "NEXT_PUBLIC_DOCSEARCH_API_KEY", From 9cd7a25343e6d7d439aec53562a45217154818f3 Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Thu, 13 Mar 2025 07:13:23 -0700 Subject: [PATCH 044/411] fix: fix except last (#4942) --- packages/surveys/src/lib/utils.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/surveys/src/lib/utils.ts b/packages/surveys/src/lib/utils.ts index 1fb763b376..2546141dd3 100644 --- a/packages/surveys/src/lib/utils.ts +++ b/packages/surveys/src/lib/utils.ts @@ -45,22 +45,23 @@ export const getShuffledChoicesIds = ( const otherOption = choices.find((choice) => { return choice.id === "other"; }); + const shuffledChoices = otherOption ? [...choices.filter((choice) => choice.id !== "other")] : [...choices]; if (shuffleOption === "all") { shuffle(shuffledChoices); - } else if (shuffleOption === "exceptLast") { - if (otherOption) { + } + if (shuffleOption === "exceptLast") { + const lastElement = shuffledChoices.pop(); + if (lastElement) { shuffle(shuffledChoices); - } else { - const lastElement = shuffledChoices.pop(); - if (lastElement) { - shuffle(shuffledChoices); - shuffledChoices.push(lastElement); - } + shuffledChoices.push(lastElement); } } - if (otherOption) shuffledChoices.push(otherOption); + + if (otherOption) { + shuffledChoices.push(otherOption); + } return shuffledChoices.map((choice) => choice.id); }; From 7103ec9877edf026425af4bdc02f71afc0ebc4ce Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Thu, 13 Mar 2025 20:34:45 +0100 Subject: [PATCH 045/411] fix: survey preview stuck in sending (#4941) --- .../settings/components/ProjectSettings.tsx | 1 + .../editor/components/survey-editor.tsx | 1 - .../survey/link/components/link-survey.tsx | 6 +- .../components/template-container.tsx | 1 - .../ui/components/preview-survey/index.tsx | 12 +-- .../theme-styling-preview-survey/index.tsx | 2 + .../surveys/src/components/general/survey.tsx | 100 ++++++++++++++---- packages/types/formbricks-surveys.ts | 1 + 8 files changed, 88 insertions(+), 36 deletions(-) diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.tsx index 475249e6ce..cf85a9fd78 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings.tsx @@ -231,6 +231,7 @@ export const ProjectSettings = ({

{t("common.preview")}

file.name} />
diff --git a/apps/web/modules/survey/link/components/link-survey.tsx b/apps/web/modules/survey/link/components/link-survey.tsx index 392d59d2be..a018222dca 100644 --- a/apps/web/modules/survey/link/components/link-survey.tsx +++ b/apps/web/modules/survey/link/components/link-survey.tsx @@ -170,14 +170,14 @@ export const LinkSurvey = ({ PRIVACY_URL={PRIVACY_URL} isBrandingEnabled={project.linkSurveyBranding}> `https://formbricks.com/${file.name}` : undefined} // eslint-disable-next-line jsx-a11y/no-autofocus -- need it as focus behaviour is different in normal surveys and survey preview autoFocus={autoFocus} prefillResponseData={prefillValue} diff --git a/apps/web/modules/survey/templates/components/template-container.tsx b/apps/web/modules/survey/templates/components/template-container.tsx index b8a7b30467..b5371d4777 100644 --- a/apps/web/modules/survey/templates/components/template-container.tsx +++ b/apps/web/modules/survey/templates/components/template-container.tsx @@ -84,7 +84,6 @@ export const TemplateContainerWithPreview = ({ project={project} environment={environment} languageCode={"default"} - onFileUpload={async (file) => file.name} /> )} diff --git a/apps/web/modules/ui/components/preview-survey/index.tsx b/apps/web/modules/ui/components/preview-survey/index.tsx index 96f0d7107a..70aeddf9cd 100644 --- a/apps/web/modules/ui/components/preview-survey/index.tsx +++ b/apps/web/modules/ui/components/preview-survey/index.tsx @@ -9,9 +9,7 @@ import { useTranslate } from "@tolgee/react"; import { Variants, motion } from "framer-motion"; import { ExpandIcon, MonitorIcon, ShrinkIcon, SmartphoneIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { TJsFileUploadParams } from "@formbricks/types/js"; import { TProjectStyling } from "@formbricks/types/project"; -import { TUploadFileConfig } from "@formbricks/types/storage"; import { TSurvey, TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types"; import { Modal } from "./components/modal"; import { TabOption } from "./components/tab-option"; @@ -25,7 +23,6 @@ interface PreviewSurveyProps { project: Project; environment: Pick; languageCode: string; - onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise; } let surveyNameTemp: string; @@ -66,7 +63,6 @@ export const PreviewSurvey = ({ project, environment, languageCode, - onFileUpload, }: PreviewSurveyProps) => { const [isModalOpen, setIsModalOpen] = useState(true); const [isFullScreenPreview, setIsFullScreenPreview] = useState(false); @@ -265,11 +261,11 @@ export const PreviewSurvey = ({ borderRadius={styling?.roundness ?? 8} background={styling?.cardBackgroundColor?.light}>
{ + if (isPreviewMode) { + // return mock url since an url is required for the preview + return `https://example.com/${file.name}`; + } + if (!apiClient) { throw new Error("apiClient not initialized"); } @@ -206,6 +211,17 @@ export function Survey({ }, [questionId]); const createDisplay = useCallback(async () => { + // Skip display creation in preview mode but still trigger the onDisplayCreated callback + if (isPreviewMode) { + if (onDisplayCreated) { + onDisplayCreated(); + } + if (onDisplay) { + onDisplay(); + } + return; + } + if (apiClient && surveyState && responseQueue) { try { const display = await apiClient.createDisplay({ @@ -229,7 +245,17 @@ export function Survey({ console.error("error creating display: ", err); } } - }, [apiClient, surveyState, responseQueue, survey.id, userId, contactId, onDisplayCreated]); + }, [ + apiClient, + surveyState, + responseQueue, + survey.id, + userId, + contactId, + onDisplayCreated, + isPreviewMode, + onDisplay, + ]); useEffect(() => { // call onDisplay when component is mounted @@ -385,6 +411,32 @@ export function Survey({ const onResponseCreateOrUpdate = useCallback( (responseUpdate: TResponseUpdate) => { + // Always trigger the onResponse callback even in preview mode + if (!apiHost || !environmentId) { + onResponse?.({ + data: responseUpdate.data, + ttc: responseUpdate.ttc, + finished: responseUpdate.finished, + variables: responseUpdate.variables, + language: responseUpdate.language, + endingId: responseUpdate.endingId, + }); + return; + } + + // Skip response creation in preview mode but still trigger the onResponseCreated callback + if (isPreviewMode) { + if (onResponseCreated) { + onResponseCreated(); + } + + // When in preview mode, set isResponseSendingFinished to true if the response is finished + if (responseUpdate.finished) { + setIsResponseSendingFinished(true); + } + return; + } + if (surveyState && responseQueue) { if (contactId) { surveyState.updateContactId(contactId); @@ -415,7 +467,20 @@ export function Survey({ } } }, - [surveyState, responseQueue, contactId, userId, survey, action, hiddenFieldsRecord, onResponseCreated] + [ + apiHost, + environmentId, + isPreviewMode, + surveyState, + responseQueue, + contactId, + userId, + survey, + action, + hiddenFieldsRecord, + onResponseCreated, + onResponse, + ] ); useEffect(() => { @@ -446,25 +511,14 @@ export function Survey({ onChange(surveyResponseData); onChangeVariables(calculatedVariables); - if (apiHost && environmentId) { - onResponseCreateOrUpdate({ - data: surveyResponseData, - ttc: responsettc, - finished, - variables: calculatedVariables, - language: selectedLanguage, - endingId, - }); - } else { - onResponse?.({ - data: surveyResponseData, - ttc: responsettc, - finished, - variables: calculatedVariables, - language: selectedLanguage, - endingId, - }); - } + onResponseCreateOrUpdate({ + data: surveyResponseData, + ttc: responsettc, + finished, + variables: calculatedVariables, + language: selectedLanguage, + endingId, + }); if (nextQuestionId) { setQuestionId(nextQuestionId); @@ -573,7 +627,7 @@ export function Survey({ onBack={onBack} ttc={ttc} setTtc={setTtc} - onFileUpload={apiHost && environmentId ? onFileUploadApi : onFileUpload!} + onFileUpload={onFileUpload ?? onFileUploadApi} isFirstQuestion={question.id === localSurvey.questions[0]?.id} skipPrefilled={skipPrefilled} prefilledQuestionValue={getQuestionPrefillData(question.id, offset)} diff --git a/packages/types/formbricks-surveys.ts b/packages/types/formbricks-surveys.ts index 26f7af4dfe..43f4e6a071 100644 --- a/packages/types/formbricks-surveys.ts +++ b/packages/types/formbricks-surveys.ts @@ -45,6 +45,7 @@ export interface SurveyModalProps extends SurveyBaseProps { export interface SurveyContainerProps extends Omit { apiHost?: string; environmentId?: string; + isPreviewMode?: boolean; userId?: string; contactId?: string; onDisplayCreated?: () => void | Promise; From 05f1068e010d0c351fba5169a0b55ed317ada9f7 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Thu, 13 Mar 2025 20:35:51 +0100 Subject: [PATCH 046/411] chore: prepare 3.3.2 release (#4930) --- apps/web/package.json | 2 +- packages/android/.gitignore | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 2b73f19a24..1fce1218e6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/web", - "version": "3.3.1", + "version": "3.3.2", "private": true, "scripts": { "clean": "rimraf .turbo node_modules .next", diff --git a/packages/android/.gitignore b/packages/android/.gitignore index aa724b7707..faf530b2d5 100644 --- a/packages/android/.gitignore +++ b/packages/android/.gitignore @@ -1,12 +1,7 @@ *.iml .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +/.idea/ .DS_Store /build /captures From c28de7c07970ee88d6866b9b10c136d5f5880217 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Thu, 13 Mar 2025 20:38:32 +0100 Subject: [PATCH 047/411] chore: prepare 3.4.0 release (#4950) --- apps/web/package.json | 2 +- helm-chart/Chart.yaml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 1fce1218e6..7a8e0245ef 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/web", - "version": "3.3.2", + "version": "3.4.0", "private": true, "scripts": { "clean": "rimraf .turbo node_modules .next", diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index 7fb8b04f60..ced5069a8e 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -5,8 +5,8 @@ description: A Helm chart for Formbricks with PostgreSQL, Redis type: application # Helm chart Version -version: 3.3.1 -appVersion: v3.3.1 +version: 3.4.0 +appVersion: v3.4.0 keywords: - formbricks @@ -18,7 +18,6 @@ maintainers: - name: Formbricks email: info@formbricks.com - dependencies: - name: postgresql version: "16.4.16" From dbbd77a8eb91d40205a0ef5921271840423caa7a Mon Sep 17 00:00:00 2001 From: Piyush Jain <122745947+d3vb0ox@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:50:07 +0530 Subject: [PATCH 048/411] chore(env): add new env variables (#4959) --- .github/workflows/release-helm-chart.yml | 43 ++++ .../workflows/terrafrom-plan-and-apply.yml | 92 +++++++++ helm-chart/Chart.yaml | 3 +- helm-chart/README.md | 2 +- helm-chart/templates/deployment.yaml | 42 +--- helm-chart/templates/hpa.yaml | 4 - helm-chart/values.yaml | 6 +- infra/terraform/data.tf | 8 + infra/terraform/iam.tf | 2 +- infra/terraform/main.tf | 185 +++++++++--------- infra/terraform/secrets.tf | 23 +-- 11 files changed, 254 insertions(+), 156 deletions(-) create mode 100644 .github/workflows/release-helm-chart.yml create mode 100644 .github/workflows/terrafrom-plan-and-apply.yml diff --git a/.github/workflows/release-helm-chart.yml b/.github/workflows/release-helm-chart.yml new file mode 100644 index 0000000000..4cb25c900b --- /dev/null +++ b/.github/workflows/release-helm-chart.yml @@ -0,0 +1,43 @@ +name: Publish Helm Chart + +on: + release: + types: + - published + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract release version + run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: latest + + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin + + - name: Install YQ + uses: dcarbone/install-yq-action@v1.3.1 + + - name: Update Chart.yaml with new version + run: | + yq -i ".version = \"${VERSION#v}\"" helm-chart/Chart.yaml + yq -i ".appVersion = \"${VERSION}\"" helm-chart/Chart.yaml + + - name: Package Helm chart + run: | + helm package ./helm-chart + + - name: Push Helm chart to GitHub Container Registry + run: | + helm push formbricks-${VERSION#v}.tgz oci://ghcr.io/formbricks/helm-charts diff --git a/.github/workflows/terrafrom-plan-and-apply.yml b/.github/workflows/terrafrom-plan-and-apply.yml new file mode 100644 index 0000000000..33c2f9e748 --- /dev/null +++ b/.github/workflows/terrafrom-plan-and-apply.yml @@ -0,0 +1,92 @@ +name: 'Terraform' + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + paths: + - 'infra/terraform/**' + +permissions: + id-token: write + contents: write + +jobs: + terraform: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }} + aws-region: "eu-central-1" + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + + - name: Terraform Format + id: fmt + run: terraform fmt -check -recursive + continue-on-error: true + working-directory: infra/terraform + +# - name: Post Format +# if: always() && github.ref != 'refs/heads/main' && (steps.fmt.outcome == 'success' || steps.fmt.outcome == 'failure') +# uses: robburger/terraform-pr-commenter@v1 +# with: +# commenter_type: fmt +# commenter_input: ${{ format('{0}{1}', steps.fmt.outputs.stdout, steps.fmt.outputs.stderr) }} +# commenter_exitcode: ${{ steps.fmt.outputs.exitcode }} + + - name: Terraform Init + id: init + run: terraform init + working-directory: infra/terraform + +# - name: Post Init +# if: always() && github.ref != 'refs/heads/main' && (steps.init.outcome == 'success' || steps.init.outcome == 'failure') +# uses: robburger/terraform-pr-commenter@v1 +# with: +# commenter_type: init +# commenter_input: ${{ format('{0}{1}', steps.init.outputs.stdout, steps.init.outputs.stderr) }} +# commenter_exitcode: ${{ steps.init.outputs.exitcode }} + + - name: Terraform Validate + id: validate + run: terraform validate + working-directory: infra/terraform + +# - name: Post Validate +# if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure') +# uses: robburger/terraform-pr-commenter@v1 +# with: +# commenter_type: validate +# commenter_input: ${{ format('{0}{1}', steps.validate.outputs.stdout, steps.validate.outputs.stderr) }} +# commenter_exitcode: ${{ steps.validate.outputs.exitcode }} + + - name: Terraform Plan + id: plan + run: terraform plan -out .planfile + working-directory: infra/terraform + + - name: Post PR comment + uses: borchero/terraform-plan-comment@v2 + if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure') + with: + token: ${{ github.token }} + planfile: .planfile + working-directory: "infra/terraform" + skip-comment: true + + - name: Terraform Apply + id: apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply .planfile + diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index ced5069a8e..5176281925 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -5,8 +5,7 @@ description: A Helm chart for Formbricks with PostgreSQL, Redis type: application # Helm chart Version -version: 3.4.0 -appVersion: v3.4.0 +version: 0.0.0-dev keywords: - formbricks diff --git a/helm-chart/README.md b/helm-chart/README.md index 8138cf9206..f76a491d66 100644 --- a/helm-chart/README.md +++ b/helm-chart/README.md @@ -1,6 +1,6 @@ # formbricks -![Version: 3.3.1](https://img.shields.io/badge/Version-3.3.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.3.1](https://img.shields.io/badge/AppVersion-v3.3.1-informational?style=flat-square) +![Version: 0.0.0-dev](https://img.shields.io/badge/Version-0.0.0--dev-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) A Helm chart for Formbricks with PostgreSQL, Redis diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml index e048576bad..378233d183 100644 --- a/helm-chart/templates/deployment.yaml +++ b/helm-chart/templates/deployment.yaml @@ -94,8 +94,12 @@ spec: protocol: {{ $config.protocol | default "TCP" | quote }} {{- end }} {{- end }} - {{- if .Values.deployment.envFrom }} + {{- if or .Values.deployment.envFrom (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }} envFrom: + {{- if or .Values.secret.enabled (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }} + - secretRef: + name: {{ template "formbricks.name" . }}-app-secrets + {{- end }} {{- range $value := .Values.deployment.envFrom }} {{- if (eq .type "configmap") }} - configMapRef: @@ -122,42 +126,8 @@ spec: env: {{- if and (.Values.enterprise.enabled) (ne .Values.enterprise.licenseKey "") }} - name: ENTERPRISE_LICENSE_KEY - valueFrom: - secretKeyRef: - name: {{ template "formbricks.name" . }}-app-secrets - key: ENTERPRISE_LICENSE_KEY - {{- else if and (.Values.enterprise.enabled) (eq .Values.enterprise.licenseKey "") }} - - name: ENTERPRISE_LICENSE_KEY - valueFrom: - secretKeyRef: - name: {{ template "formbricks.name" . }}-app-secrets - key: ENTERPRISE_LICENSE_KEY + value: {{ .Values.enterprise.licenseKey | quote }} {{- end }} - - name: REDIS_URL - valueFrom: - secretKeyRef: - name: {{ template "formbricks.name" . }}-app-secrets - key: REDIS_URL - - name: DATABASE_URL - valueFrom: - secretKeyRef: - name: {{ template "formbricks.name" . }}-app-secrets - key: DATABASE_URL - - name: CRON_SECRET - valueFrom: - secretKeyRef: - name: {{ template "formbricks.name" . }}-app-secrets - key: CRON_SECRET - - name: ENCRYPTION_KEY - valueFrom: - secretKeyRef: - name: {{ template "formbricks.name" . }}-app-secrets - key: ENCRYPTION_KEY - - name: NEXTAUTH_SECRET - valueFrom: - secretKeyRef: - name: {{ template "formbricks.name" . }}-app-secrets - key: NEXTAUTH_SECRET {{- range $key, $value := .Values.deployment.env }} - name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }} {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | indent 10 }} diff --git a/helm-chart/templates/hpa.yaml b/helm-chart/templates/hpa.yaml index 5adb814124..d2567401c0 100644 --- a/helm-chart/templates/hpa.yaml +++ b/helm-chart/templates/hpa.yaml @@ -1,10 +1,6 @@ {{- if .Values.autoscaling.enabled }} --- -{{- if .Capabilities.APIVersions.Has "autoscaling/v2/HorizontalPodAutoscaler" }} apiVersion: autoscaling/v2 -{{- else }} -apiVersion: autoscaling/v2beta2 -{{- end }} kind: HorizontalPodAutoscaler metadata: name: {{ template "formbricks.name" . }} diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 379cad50f4..684b3ec130 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -54,9 +54,9 @@ deployment: # Environment variables from ConfigMaps or Secrets envFrom: - # app-secrets: - # type: secret - # nameSuffix: app-secrets + # app-secrets: + # type: secret + # nameSuffix: app-secrets # Environment variables passed to the app container env: diff --git a/infra/terraform/data.tf b/infra/terraform/data.tf index fcf818d66e..cecb31932e 100644 --- a/infra/terraform/data.tf +++ b/infra/terraform/data.tf @@ -10,3 +10,11 @@ data "aws_eks_cluster_auth" "eks" { data "aws_ecrpublic_authorization_token" "token" { provider = aws.virginia } + +data "aws_iam_roles" "administrator" { + name_regex = "AWSReservedSSO_AdministratorAccess" +} + +data "aws_iam_roles" "github" { + name_regex = "formbricks-prod-github" +} diff --git a/infra/terraform/iam.tf b/infra/terraform/iam.tf index f0af377861..27ac846e14 100644 --- a/infra/terraform/iam.tf +++ b/infra/terraform/iam.tf @@ -23,7 +23,7 @@ module "iam_github_oidc_role" { "repo:formbricks/*:*", ] policies = { - Administrator = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" + Administrator = "arn:aws:iam::aws:policy/AdministratorAccess" } tags = local.tags diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf index c1327e5376..1f020e7f5c 100644 --- a/infra/terraform/main.tf +++ b/infra/terraform/main.tf @@ -249,7 +249,7 @@ module "eks" { cluster_name = "${local.name}-eks" cluster_version = "1.32" - enable_cluster_creator_admin_permissions = true + enable_cluster_creator_admin_permissions = false cluster_endpoint_public_access = true cluster_addons = { @@ -271,6 +271,41 @@ module "eks" { } } + kms_key_administrators = [ + tolist(data.aws_iam_roles.github.arns)[0], + tolist(data.aws_iam_roles.administrator.arns)[0] + ] + + kms_key_users = [ + tolist(data.aws_iam_roles.github.arns)[0], + tolist(data.aws_iam_roles.administrator.arns)[0] + ] + + access_entries = { + administrator = { + principal_arn = tolist(data.aws_iam_roles.administrator.arns)[0] + policy_associations = { + Admin = { + policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + access_scope = { + type = "cluster" + } + } + } + } + github = { + principal_arn = tolist(data.aws_iam_roles.github.arns)[0] + policy_associations = { + Admin = { + policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + access_scope = { + type = "cluster" + } + } + } + } + } + vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets control_plane_subnet_ids = module.vpc.intra_subnets @@ -573,95 +608,69 @@ resource "helm_release" "formbricks" { values = [ <<-EOT - postgresql: - enabled: false - redis: - enabled: false - ingress: + postgresql: + enabled: false + redis: + enabled: false + ingress: + enabled: true + ingressClassName: alb + hosts: + - host: "app.${local.domain}" + paths: + - path: / + pathType: "Prefix" + serviceName: "formbricks" + annotations: + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/target-type: ip + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/certificate-arn: ${module.acm.acm_certificate_arn} + alb.ingress.kubernetes.io/healthcheck-path: "/health" + alb.ingress.kubernetes.io/group.name: formbricks + alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06" + secret: + enabled: false + rbac: + enabled: true + serviceAccount: enabled: true - ingressClassName: alb - hosts: - - host: "app.${local.domain}" - paths: - - path: / - pathType: "Prefix" - serviceName: "formbricks" + name: formbricks annotations: - alb.ingress.kubernetes.io/scheme: internet-facing - alb.ingress.kubernetes.io/target-type: ip - alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' - alb.ingress.kubernetes.io/ssl-redirect: "443" - alb.ingress.kubernetes.io/certificate-arn: ${module.acm.acm_certificate_arn} - alb.ingress.kubernetes.io/healthcheck-path: "/health" - alb.ingress.kubernetes.io/group.name: formbricks - alb.ingress.kubernetes.io/ssl-policy: "ELBSecurityPolicy-TLS13-1-2-2021-06" - secret: - enabled: false - rbac: - enabled: true - serviceAccount: - enabled: true - name: formbricks - annotations: - eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn} - serviceMonitor: - enabled: true - deployment: - image: - repository: "ghcr.io/formbricks/formbricks-experimental" - tag: "open-telemetry-for-prometheus" - pullPolicy: Always - env: - S3_BUCKET_NAME: - value: ${module.s3-bucket.s3_bucket_id} - RATE_LIMITING_DISABLED: - value: "1" - envFrom: - app-parameters: - type: secret - nameSuffix: {RELEASE.name}-app-parameters - annotations: - deployed_at: ${timestamp()} - externalSecret: - enabled: true # Enable/disable ExternalSecrets - secretStore: - name: aws-secrets-manager - kind: ClusterSecretStore - refreshInterval: "1h" - files: - app-parameters: - dataFrom: - key: "/prod/formbricks/env" - secretStore: - name: aws-parameter-store - kind: ClusterSecretStore - app-secrets: - data: - DATABASE_URL: - remoteRef: - key: "prod/formbricks/secrets" - property: DATABASE_URL - REDIS_URL: - remoteRef: - key: "prod/formbricks/secrets" - property: REDIS_URL - CRON_SECRET: - remoteRef: - key: "prod/formbricks/secrets" - property: CRON_SECRET - ENCRYPTION_KEY: - remoteRef: - key: "prod/formbricks/secrets" - property: ENCRYPTION_KEY - NEXTAUTH_SECRET: - remoteRef: - key: "prod/formbricks/secrets" - property: NEXTAUTH_SECRET - ENTERPRISE_LICENSE_KEY: - remoteRef: - key: "prod/formbricks/enterprise" - property: ENTERPRISE_LICENSE_KEY - EOT + eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn} + serviceMonitor: + enabled: true + deployment: + image: + repository: "ghcr.io/formbricks/formbricks-experimental" + tag: "open-telemetry-for-prometheus" + pullPolicy: Always + env: + S3_BUCKET_NAME: + value: ${module.s3-bucket.s3_bucket_id} + RATE_LIMITING_DISABLED: + value: "1" + envFrom: + app-env: + type: secret + nameSuffix: app-env + annotations: + deployed_at: ${timestamp()} + externalSecret: + enabled: true # Enable/disable ExternalSecrets + secretStore: + name: aws-secrets-manager + kind: ClusterSecretStore + refreshInterval: "1h" + files: + app-env: + dataFrom: + key: "prod/formbricks/environment" + app-secrets: + dataFrom: + key: "prod/formbricks/secrets" + EOT ] } diff --git a/infra/terraform/secrets.tf b/infra/terraform/secrets.tf index ec98262af1..3a633bfc55 100644 --- a/infra/terraform/secrets.tf +++ b/infra/terraform/secrets.tf @@ -1,19 +1,3 @@ -# Generate random secrets for formbricks -resource "random_password" "nextauth_secret" { - length = 32 - special = false -} - -resource "random_password" "encryption_key" { - length = 32 - special = false -} - -resource "random_password" "cron_secret" { - length = 32 - special = false -} - # Create the first AWS Secrets Manager secret for environment variables resource "aws_secretsmanager_secret" "formbricks_app_secrets" { name = "prod/formbricks/secrets" @@ -24,10 +8,7 @@ resource "aws_secretsmanager_secret" "formbricks_app_secrets" { resource "aws_secretsmanager_secret_version" "formbricks_app_secrets" { secret_id = aws_secretsmanager_secret.formbricks_app_secrets.id secret_string = jsonencode({ - NEXTAUTH_SECRET = random_password.nextauth_secret.result - ENCRYPTION_KEY = random_password.encryption_key.result - CRON_SECRET = random_password.cron_secret.result - DATABASE_URL = "postgres://formbricks:${random_password.postgres.result}@${module.rds-aurora.cluster_endpoint}/formbricks" - REDIS_URL = "rediss://:${random_password.valkey.result}@${module.elasticache.replication_group_primary_endpoint_address}:6379" + DATABASE_URL = "postgres://formbricks:${random_password.postgres.result}@${module.rds-aurora.cluster_endpoint}/formbricks" + REDIS_URL = "rediss://:${random_password.valkey.result}@${module.elasticache.replication_group_primary_endpoint_address}:6379" }) } From a371bdaedde0f98809b43186d2934f8c906ed3a9 Mon Sep 17 00:00:00 2001 From: Piyush Jain <122745947+d3vb0ox@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:02:05 +0530 Subject: [PATCH 049/411] chore(terraform): fix (#4963) --- .github/workflows/terrafrom-plan-and-apply.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/terrafrom-plan-and-apply.yml b/.github/workflows/terrafrom-plan-and-apply.yml index 33c2f9e748..25b1422d0f 100644 --- a/.github/workflows/terrafrom-plan-and-apply.yml +++ b/.github/workflows/terrafrom-plan-and-apply.yml @@ -89,4 +89,5 @@ jobs: id: apply if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: terraform apply .planfile + working-directory: "infra/terraform" From c2d237a99a22ffa347804080188d896d570d01cb Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:40:51 +0530 Subject: [PATCH 050/411] fix: google sheet integration error message (#4899) --- .../integrations/google-sheets/actions.ts | 54 ++++++++++++------- .../components/AddIntegrationModal.tsx | 14 +++-- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts index a95358353e..9871377253 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts @@ -1,25 +1,41 @@ "use server"; -import { authOptions } from "@/modules/auth/lib/authOptions"; -import { getServerSession } from "next-auth"; -import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { authenticatedActionClient } from "@/lib/utils/action-client"; +import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"; +import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper"; +import { z } from "zod"; import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service"; -import { AuthorizationError } from "@formbricks/types/errors"; -import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; +import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; -export async function getSpreadsheetNameByIdAction( - googleSheetIntegration: TIntegrationGoogleSheets, - environmentId: string, - spreadsheetId: string -) { - const session = await getServerSession(authOptions); - if (!session) throw new AuthorizationError("Not authorized"); +const ZGetSpreadsheetNameByIdAction = z.object({ + googleSheetIntegration: ZIntegrationGoogleSheets, + environmentId: z.string(), + spreadsheetId: z.string(), +}); - const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); - if (!isAuthorized) throw new AuthorizationError("Not authorized"); - const integrationData = structuredClone(googleSheetIntegration); - integrationData.config.data.forEach((data) => { - data.createdAt = new Date(data.createdAt); +export const getSpreadsheetNameByIdAction = authenticatedActionClient + .schema(ZGetSpreadsheetNameByIdAction) + .action(async ({ ctx, parsedInput }) => { + await checkAuthorizationUpdated({ + userId: ctx.user.id, + organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId), + access: [ + { + type: "organization", + roles: ["owner", "manager"], + }, + { + type: "projectTeam", + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), + minPermission: "readWrite", + }, + ], + }); + + const integrationData = structuredClone(parsedInput.googleSheetIntegration); + integrationData.config.data.forEach((data) => { + data.createdAt = new Date(data.createdAt); + }); + + return await getSpreadsheetNameById(integrationData, parsedInput.spreadsheetId); }); - return await getSpreadsheetNameById(integrationData, spreadsheetId); -} diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx index 5fe5dc54e4..3c1a01314c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx @@ -8,6 +8,7 @@ import { isValidGoogleSheetsUrl, } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util"; import GoogleSheetLogo from "@/images/googleSheetsLogo.png"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings"; import { Button } from "@/modules/ui/components/button"; import { Checkbox } from "@/modules/ui/components/checkbox"; @@ -115,11 +116,18 @@ export const AddIntegrationModal = ({ throw new Error(t("environments.integrations.select_at_least_one_question_error")); } const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl); - const spreadsheetName = await getSpreadsheetNameByIdAction( + const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({ googleSheetIntegration, environmentId, - spreadsheetId - ); + spreadsheetId, + }); + + if (!spreadsheetNameResponse?.data) { + const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse); + throw new Error(errorMessage); + } + + const spreadsheetName = spreadsheetNameResponse.data; setIsLinkingSheet(true); integrationData.spreadsheetId = spreadsheetId; From aa910ca3f07d0132abbca260d39edf4e7f4a6320 Mon Sep 17 00:00:00 2001 From: victorvhs017 <115753265+victorvhs017@users.noreply.github.com> Date: Mon, 17 Mar 2025 06:33:02 -0300 Subject: [PATCH 051/411] fix: updated docker file with redis and minio containers (#4909) --- .env.example | 2 +- .gitpod/init.bash | 2 +- apps/web/cache-handler.mjs | 4 +-- docker-compose.dev.yml | 44 ++++++++++++++++++++++++++++ package.json | 4 ++- packages/database/docker-compose.yml | 21 ------------- packages/database/package.json | 4 +-- turbo.json | 7 +++++ 8 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 docker-compose.dev.yml delete mode 100644 packages/database/docker-compose.yml diff --git a/.env.example b/.env.example index 6243260549..8564c8edc7 100644 --- a/.env.example +++ b/.env.example @@ -185,7 +185,7 @@ ENTERPRISE_LICENSE_KEY= UNSPLASH_ACCESS_KEY= # The below is used for Next Caching (uses In-Memory from Next Cache if not provided) -# REDIS_URL=redis://localhost:6379 +REDIS_URL=redis://localhost:6379 # The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this) # REDIS_HTTP_URL: diff --git a/.gitpod/init.bash b/.gitpod/init.bash index 3aa3ad62fa..8b02b9dddb 100644 --- a/.gitpod/init.bash +++ b/.gitpod/init.bash @@ -1,6 +1,6 @@ #!/bin/bash -images=($(yq eval '.services.*.image' packages/database/docker-compose.yml)) +images=($(yq eval '.services.*.image' docker-compose.dev.yml)) pull_image() { docker pull "$1" diff --git a/apps/web/cache-handler.mjs b/apps/web/cache-handler.mjs index aa655c13e5..bf30a8df4f 100644 --- a/apps/web/cache-handler.mjs +++ b/apps/web/cache-handler.mjs @@ -11,7 +11,7 @@ const createTimeoutPromise = (ms, rejectReason) => { CacheHandler.onCreation(async () => { let client; - if (process.env.REDIS_URL && process.env.ENTERPRISE_LICENSE_KEY) { + if (process.env.REDIS_URL) { try { // Create a Redis client. client = createClient({ @@ -45,8 +45,6 @@ CacheHandler.onCreation(async () => { }); } } - } else if (process.env.REDIS_URL) { - console.log("Redis clustering requires an Enterprise License. Falling back to LRU cache."); } /** @type {import("@neshca/cache-handler").Handler | null} */ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000000..65a88080e5 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,44 @@ +services: + postgres: + image: pgvector/pgvector:pg17 + volumes: + - postgres:/var/lib/postgresql/data + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + ports: + - 5432:5432 + + mailhog: + image: arjenz/mailhog # Copy of mailhog/MailHog to support linux/arm64 + ports: + - 8025:8025 # web ui + - 1025:1025 # smtp server + + redis: + image: redis:7.0.11 + ports: + - 6379:6379 + volumes: + - redis-data:/data + + minio: + image: minio/minio:RELEASE.2025-02-28T09-55-16Z + command: server /data --console-address ":9001" + environment: + - MINIO_ROOT_USER=devminio + - MINIO_ROOT_PASSWORD=devminio123 + ports: + - "9000:9000" # S3 API + - "9001:9001" # Console + volumes: + - minio-data:/data + +volumes: + postgres: + driver: local + redis-data: + driver: local + minio-data: + driver: local diff --git a/package.json b/package.json index 2d29bd2822..072c87cba1 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "db:migrate:deploy": "turbo run db:migrate:deploy", "db:start": "turbo run db:start", "db:push": "turbo run db:push", - "go": "turbo run go --concurrency 20", + "db:up": "docker compose -f docker-compose.dev.yml up -d", + "db:down": "docker compose -f docker-compose.dev.yml down", + "go": "pnpm db:up && turbo run go --concurrency 20", "dev": "turbo run dev --parallel", "pre-commit": "lint-staged", "start": "turbo run start --parallel", diff --git a/packages/database/docker-compose.yml b/packages/database/docker-compose.yml deleted file mode 100644 index 6b41cd0d30..0000000000 --- a/packages/database/docker-compose.yml +++ /dev/null @@ -1,21 +0,0 @@ -services: - postgres: - image: pgvector/pgvector:pg17 - volumes: - - postgres:/var/lib/postgresql/data - environment: - - POSTGRES_DB=postgres - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - ports: - - 5432:5432 - - mailhog: - image: arjenz/mailhog # Copy of mailhog/MailHog to support linux/arm64 - ports: - - 8025:8025 # web ui - - 1025:1025 # smtp server - -volumes: - postgres: - driver: local diff --git a/packages/database/package.json b/packages/database/package.json index a49df58721..96f6896a01 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -13,10 +13,8 @@ "db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" tsx ./src/scripts/create-saml-database.ts", "db:create-saml-database:dev": "dotenv -e ../../.env -- tsx ./src/scripts/create-saml-database.ts", "db:push": "prisma db push --accept-data-loss", - "db:up": "docker compose up -d", - "db:setup": "pnpm db:up && pnpm db:migrate:dev && pnpm db:create-saml-database:dev", + "db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev", "db:start": "pnpm db:setup", - "db:down": "docker compose down", "format": "prisma format", "generate": "prisma generate", "lint": "eslint ./src --fix", diff --git a/turbo.json b/turbo.json index c8ac3cc8fe..be2d78dc9a 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,9 @@ "dependsOn": ["@formbricks/api#build"], "persistent": true }, + "@formbricks/database#setup": { + "dependsOn": ["db:up"] + }, "@formbricks/demo#go": { "cache": false, "dependsOn": ["@formbricks/js#build"], @@ -224,6 +227,10 @@ "db:start": { "cache": false }, + "db:up": { + "cache": false, + "outputs": [] + }, "dev": { "cache": false, "persistent": true From e5ce6532f5bfdf3d8589009c5f28a8e1c167dfc4 Mon Sep 17 00:00:00 2001 From: Peter Pesti-Varga Date: Mon, 17 Mar 2025 13:13:05 +0100 Subject: [PATCH 052/411] fix: Fix Android build setting (#4967) --- packages/android/app/build.gradle.kts | 2 +- .../android/app/src/main/AndroidManifest.xml | 1 - .../main/res/mipmap-anydpi/ic_launcher.xml | 6 ----- .../res/mipmap-anydpi/ic_launcher_round.xml | 6 ----- .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 0 bytes .../android/formbricksSDK/build.gradle.kts | 22 ++++++++++++--- .../android/formbricksSDK/consumer-rules.pro | 2 ++ .../formbricks/formbrickssdk/Formbricks.kt | 23 +++++++--------- .../extensions/DateExtensions.kt | 21 +++++++-------- .../formbricks/formbrickssdk/logger/Logger.kt | 24 +++++++++++++++++ .../formbrickssdk/manager/SurveyManager.kt | 25 +++++++++--------- .../formbrickssdk/manager/UserManager.kt | 4 +-- .../network/queue/UpdateQueue.kt | 6 ++--- .../webview/FormbricksFragment.kt | 12 +++------ .../formbrickssdk/webview/WebAppInterface.kt | 12 ++++----- packages/android/gradle/libs.versions.toml | 3 --- 20 files changed, 91 insertions(+), 78 deletions(-) delete mode 100644 packages/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml delete mode 100644 packages/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml delete mode 100644 packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 packages/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 packages/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/logger/Logger.kt diff --git a/packages/android/app/build.gradle.kts b/packages/android/app/build.gradle.kts index d2bf794cfa..8ea81d2ebb 100644 --- a/packages/android/app/build.gradle.kts +++ b/packages/android/app/build.gradle.kts @@ -11,7 +11,7 @@ android { defaultConfig { applicationId = "com.formbricks.demo" - minSdk = 26 + minSdk = 24 targetSdk = 35 versionCode = 1 versionName = "1.0" diff --git a/packages/android/app/src/main/AndroidManifest.xml b/packages/android/app/src/main/AndroidManifest.xml index d0460a8f9c..e57bab9e9d 100644 --- a/packages/android/app/src/main/AndroidManifest.xml +++ b/packages/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,6 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" - android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Demo" tools:targetApi="31"> diff --git a/packages/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/packages/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755bf5..0000000000 --- a/packages/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/packages/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml deleted file mode 100644 index 6f3b755bf5..0000000000 --- a/packages/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/packages/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2898 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 diff --git a/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/packages/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da081676d42f6c3f78a2c91e7bcedddedb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1772 zcmVQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i diff --git a/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/packages/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s diff --git a/packages/android/formbricksSDK/build.gradle.kts b/packages/android/formbricksSDK/build.gradle.kts index f304ab3d8f..307bed78ff 100644 --- a/packages/android/formbricksSDK/build.gradle.kts +++ b/packages/android/formbricksSDK/build.gradle.kts @@ -12,7 +12,7 @@ android { compileSdk = 35 defaultConfig { - minSdk = 26 + minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -30,6 +30,24 @@ android { ) } } + + packaging { + resources { + excludes += "META-INF/library_release.kotlin_module" + excludes += "classes.dex" + excludes += "**.**" + pickFirsts += "**/DataBinderMapperImpl.java" + pickFirsts += "**/DataBinderMapperImpl.class" + pickFirsts += "**/formbrickssdk/DataBinderMapperImpl.java" + pickFirsts += "**/formbrickssdk/DataBinderMapperImpl.class" + } + } + viewBinding { + enable = true + } + dataBinding { + enable = true + } buildFeatures { dataBinding = true viewBinding = true @@ -65,8 +83,6 @@ dependencies { implementation(libs.material) - implementation(libs.timber) - implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.legacy.support.v4) implementation(libs.androidx.lifecycle.livedata.ktx) diff --git a/packages/android/formbricksSDK/consumer-rules.pro b/packages/android/formbricksSDK/consumer-rules.pro index e69de29bb2..d1518d7c57 100644 --- a/packages/android/formbricksSDK/consumer-rules.pro +++ b/packages/android/formbricksSDK/consumer-rules.pro @@ -0,0 +1,2 @@ +-keep class com.formbricks.formbrickssdk.DataBinderMapperImpl { *; } +-keep class com.formbricks.formbrickssdk.Formbricks { *; } \ No newline at end of file diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/Formbricks.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/Formbricks.kt index 35f35d7d9f..e8d6a6d5f6 100644 --- a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/Formbricks.kt +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/Formbricks.kt @@ -7,12 +7,11 @@ import androidx.annotation.Keep import androidx.fragment.app.FragmentManager import com.formbricks.formbrickssdk.api.FormbricksApi import com.formbricks.formbrickssdk.helper.FormbricksConfig +import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.manager.SurveyManager import com.formbricks.formbrickssdk.manager.UserManager import com.formbricks.formbrickssdk.model.error.SDKError import com.formbricks.formbrickssdk.webview.FormbricksFragment -import timber.log.Timber - @Keep object Formbricks { @@ -61,10 +60,6 @@ object Formbricks { SurveyManager.refreshEnvironmentIfNeeded() UserManager.syncUserStateIfNeeded() - if (loggingEnabled) { - Timber.plant(Timber.DebugTree()) - } - isInitialized = true } @@ -79,7 +74,7 @@ object Formbricks { */ fun setUserId(userId: String) { if (!isInitialized) { - Timber.e(SDKError.sdkIsNotInitialized) + Logger.e(exception = SDKError.sdkIsNotInitialized) return } UserManager.set(userId) @@ -96,7 +91,7 @@ object Formbricks { */ fun setAttribute(attribute: String, key: String) { if (!isInitialized) { - Timber.e(SDKError.sdkIsNotInitialized) + Logger.e(exception = SDKError.sdkIsNotInitialized) return } UserManager.addAttribute(attribute, key) @@ -113,7 +108,7 @@ object Formbricks { */ fun setAttributes(attributes: Map) { if (!isInitialized) { - Timber.e(SDKError.sdkIsNotInitialized) + Logger.e(exception = SDKError.sdkIsNotInitialized) return } UserManager.setAttributes(attributes) @@ -130,7 +125,7 @@ object Formbricks { */ fun setLanguage(language: String) { if (!isInitialized) { - Timber.e(SDKError.sdkIsNotInitialized) + Logger.e(exception = SDKError.sdkIsNotInitialized) return } Formbricks.language = language @@ -148,12 +143,12 @@ object Formbricks { */ fun track(action: String) { if (!isInitialized) { - Timber.e(SDKError.sdkIsNotInitialized) + Logger.e(exception = SDKError.sdkIsNotInitialized) return } if (!isInternetAvailable()) { - Timber.w(SDKError.connectionIsNotAvailable) + Logger.w(exception = SDKError.connectionIsNotAvailable) return } @@ -171,7 +166,7 @@ object Formbricks { */ fun logout() { if (!isInitialized) { - Timber.e(SDKError.sdkIsNotInitialized) + Logger.e(exception = SDKError.sdkIsNotInitialized) return } @@ -195,7 +190,7 @@ object Formbricks { /// Assembles the survey fragment and presents it internal fun showSurvey(id: String) { if (fragmentManager == null) { - Timber.e(SDKError.fragmentManagerIsNotSet) + Logger.e(exception = SDKError.fragmentManagerIsNotSet) return } diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/extensions/DateExtensions.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/extensions/DateExtensions.kt index 296c826026..eb78ed22e7 100644 --- a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/extensions/DateExtensions.kt +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/extensions/DateExtensions.kt @@ -4,9 +4,6 @@ import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder import com.formbricks.formbrickssdk.model.user.UserState import com.formbricks.formbrickssdk.model.user.UserStateData import java.text.SimpleDateFormat -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale import java.util.TimeZone @@ -22,9 +19,9 @@ fun Date.dateString(): String { fun UserStateData.lastDisplayAt(): Date? { lastDisplayAt?.let { try { - val formatter = DateTimeFormatter.ofPattern(dateFormatPattern) - val dateTime = LocalDateTime.parse(it, formatter) - return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant()) + val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault()) + formatter.timeZone = TimeZone.getTimeZone("UTC") + return formatter.parse(it) } catch (e: Exception) { return null } @@ -36,9 +33,9 @@ fun UserStateData.lastDisplayAt(): Date? { fun UserState.expiresAt(): Date? { expiresAt?.let { try { - val formatter = DateTimeFormatter.ofPattern(dateFormatPattern) - val dateTime = LocalDateTime.parse(it, formatter) - return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant()) + val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault()) + formatter.timeZone = TimeZone.getTimeZone("UTC") + return formatter.parse(it) } catch (e: Exception) { return null } @@ -50,9 +47,9 @@ fun UserState.expiresAt(): Date? { fun EnvironmentDataHolder.expiresAt(): Date? { data?.expiresAt?.let { try { - val formatter = DateTimeFormatter.ofPattern(dateFormatPattern) - val dateTime = LocalDateTime.parse(it, formatter) - return Date.from(dateTime.atZone(ZoneId.of("GMT")).toInstant()) + val formatter = SimpleDateFormat(dateFormatPattern, Locale.getDefault()) + formatter.timeZone = TimeZone.getTimeZone("UTC") + return formatter.parse(it) } catch (e: Exception) { return null } diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/logger/Logger.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/logger/Logger.kt new file mode 100644 index 0000000000..ea45dd6484 --- /dev/null +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/logger/Logger.kt @@ -0,0 +1,24 @@ +package com.formbricks.formbrickssdk.logger + +import android.util.Log +import com.formbricks.formbrickssdk.Formbricks + +object Logger { + fun d(message: String) { + if (Formbricks.loggingEnabled) { + Log.d("FormbricksSDK", message) + } + } + + fun e(message: String? = "Exception", exception: RuntimeException? = null) { + if (Formbricks.loggingEnabled) { + Log.e("FormbricksSDK", message, exception) + } + } + + fun w(message: String? = "Warning", exception: RuntimeException? = null) { + if (Formbricks.loggingEnabled) { + Log.w("FormbricksSDK", message, exception) + } + } +} \ No newline at end of file diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/SurveyManager.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/SurveyManager.kt index 9f7c30c92a..cc0a373ede 100644 --- a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/SurveyManager.kt +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/SurveyManager.kt @@ -5,6 +5,7 @@ import com.formbricks.formbrickssdk.Formbricks import com.formbricks.formbrickssdk.api.FormbricksApi import com.formbricks.formbrickssdk.extensions.expiresAt import com.formbricks.formbrickssdk.extensions.guard +import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.model.environment.EnvironmentDataHolder import com.formbricks.formbrickssdk.model.environment.Survey import com.formbricks.formbrickssdk.model.user.Display @@ -12,12 +13,10 @@ import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import timber.log.Timber -import java.time.Instant -import java.time.temporal.ChronoUnit import java.util.Date import java.util.Timer import java.util.TimerTask +import java.util.concurrent.TimeUnit /** * The SurveyManager is responsible for managing the surveys that are displayed to the user. @@ -58,7 +57,7 @@ object SurveyManager { try { Gson().fromJson(json, EnvironmentDataHolder::class.java) } catch (e: Exception) { - Timber.tag("SurveyManager").e("Unable to retrieve environment data from the local storage.") + Logger.e("Unable to retrieve environment data from the local storage.") null } } @@ -102,7 +101,7 @@ object SurveyManager { if (!force) { environmentDataHolder?.expiresAt()?.let { if (it.after(Date())) { - Timber.tag("SurveyManager").d("Environment state is still valid until $it") + Logger.d("Environment state is still valid until $it") filterSurveys() return } @@ -117,7 +116,7 @@ object SurveyManager { hasApiError = false } catch (e: Exception) { hasApiError = true - Timber.tag("SurveyManager").e(e, "Unable to refresh environment state.") + Logger.e("Unable to refresh environment state.") startErrorTimer() } } @@ -148,7 +147,7 @@ object SurveyManager { Formbricks.showSurvey(it) } - }, Date.from(Instant.now().plusSeconds(timeout.toLong()))) + }, Date(System.currentTimeMillis() + timeout.toLong() * 1000)) } } } @@ -167,7 +166,7 @@ object SurveyManager { */ fun postResponse(surveyId: String?) { val id = surveyId.guard { - Timber.tag("SurveyManager").e("Survey id is mandatory to set.") + Logger.e("Survey id is mandatory to set.") return } @@ -179,7 +178,7 @@ object SurveyManager { */ fun onNewDisplay(surveyId: String?) { val id = surveyId.guard { - Timber.tag("SurveyManager").e("Survey id is mandatory to set.") + Logger.e("Survey id is mandatory to set.") return } @@ -193,7 +192,7 @@ object SurveyManager { val date = expiresAt.guard { return } refreshTimer.schedule(object: TimerTask() { override fun run() { - Timber.tag("SurveyManager").d("Refreshing environment state.") + Logger.d("Refreshing environment state.") refreshEnvironmentIfNeeded() } @@ -207,7 +206,7 @@ object SurveyManager { val targetDate = Date(System.currentTimeMillis() + 1000 * 60 * REFRESH_STATE_ON_ERROR_TIMEOUT_IN_MINUTES) refreshTimer.schedule(object: TimerTask() { override fun run() { - Timber.tag("SurveyManager").d("Refreshing environment state after an error") + Logger.d("Refreshing environment state after an error") refreshEnvironmentIfNeeded() } @@ -240,7 +239,7 @@ object SurveyManager { } else -> { - Timber.tag("SurveyManager").e("Invalid Display Option") + Logger.e("Invalid Display Option") false } } @@ -257,7 +256,7 @@ object SurveyManager { val recontactDays = survey.recontactDays ?: defaultRecontactDays if (recontactDays != null) { - val daysBetween = ChronoUnit.DAYS.between(lastDisplayedAt.toInstant(), Instant.now()) + val daysBetween = TimeUnit.MILLISECONDS.toDays(Date().time - lastDisplayedAt.time) return@filter daysBetween >= recontactDays.toInt() } diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/UserManager.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/UserManager.kt index c3159c444a..7fcb3eb625 100644 --- a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/UserManager.kt +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/manager/UserManager.kt @@ -7,13 +7,13 @@ import com.formbricks.formbrickssdk.extensions.dateString import com.formbricks.formbrickssdk.extensions.expiresAt import com.formbricks.formbrickssdk.extensions.guard import com.formbricks.formbrickssdk.extensions.lastDisplayAt +import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.model.user.Display import com.formbricks.formbrickssdk.network.queue.UpdateQueue import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import timber.log.Timber import java.util.Date import java.util.Timer import java.util.TimerTask @@ -140,7 +140,7 @@ object UserManager { SurveyManager.filterSurveys() startSyncTimer() } catch (e: Exception) { - Timber.tag("SurveyManager").e(e, "Unable to post survey response.") + Logger.e("Unable to post survey response.") } } } diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/network/queue/UpdateQueue.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/network/queue/UpdateQueue.kt index b765cc00bd..8c6a067b17 100644 --- a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/network/queue/UpdateQueue.kt +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/network/queue/UpdateQueue.kt @@ -1,7 +1,7 @@ package com.formbricks.formbrickssdk.network.queue +import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.manager.UserManager -import timber.log.Timber import java.util.* import kotlin.concurrent.timer @@ -57,11 +57,11 @@ class UpdateQueue private constructor() { private fun commit() { val currentUserId = userId if (currentUserId == null) { - Timber.d("Error: User ID is not set yet") + Logger.d("Error: User ID is not set yet") return } - Timber.d("UpdateQueue - commit() called on UpdateQueue with $currentUserId and $attributes") + Logger.d("UpdateQueue - commit() called on UpdateQueue with $currentUserId and $attributes") UserManager.syncUser(currentUserId, attributes) } diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/FormbricksFragment.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/FormbricksFragment.kt index 847f3bc81f..33905d103f 100644 --- a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/FormbricksFragment.kt +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/FormbricksFragment.kt @@ -6,14 +6,12 @@ import android.app.Dialog import android.content.Intent import android.graphics.Color import android.net.Uri -import android.os.Build import android.os.Bundle import android.provider.OpenableColumns import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.WindowInsets import android.view.WindowManager import android.webkit.ConsoleMessage import android.webkit.WebChromeClient @@ -25,16 +23,14 @@ import androidx.fragment.app.viewModels import com.formbricks.formbrickssdk.Formbricks import com.formbricks.formbrickssdk.R import com.formbricks.formbrickssdk.databinding.FragmentFormbricksBinding +import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.manager.SurveyManager import com.formbricks.formbrickssdk.model.javascript.FileUploadData import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.gson.JsonObject -import kotlinx.serialization.json.JsonArray -import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.InputStream -import java.time.Instant import java.util.Date import java.util.Timer import java.util.TimerTask @@ -58,7 +54,8 @@ class FormbricksFragment : BottomSheetDialogFragment() { dismiss() } - }, Date.from(Instant.now().plusSeconds(CLOSING_TIMEOUT_IN_SECONDS))) + }, Date(System.currentTimeMillis() + CLOSING_TIMEOUT_IN_SECONDS * 1000) + ) } override fun onDisplayCreated() { @@ -158,7 +155,7 @@ class FormbricksFragment : BottomSheetDialogFragment() { override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { consoleMessage?.let { cm -> val log = "[CONSOLE:${cm.messageLevel()}] \"${cm.message()}\", source: ${cm.sourceId()} (${cm.lineNumber()})" - Timber.tag("Javascript message").d(log) + Logger.d(log) } return super.onConsoleMessage(consoleMessage) } @@ -229,4 +226,3 @@ class FormbricksFragment : BottomSheetDialogFragment() { private const val CLOSING_TIMEOUT_IN_SECONDS = 5L } } - diff --git a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/WebAppInterface.kt b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/WebAppInterface.kt index 8618c7bc53..c2cc9925a4 100644 --- a/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/WebAppInterface.kt +++ b/packages/android/formbricksSDK/src/main/java/com/formbricks/formbrickssdk/webview/WebAppInterface.kt @@ -1,11 +1,11 @@ package com.formbricks.formbrickssdk.webview import android.webkit.JavascriptInterface +import com.formbricks.formbrickssdk.logger.Logger import com.formbricks.formbrickssdk.model.javascript.JsMessageData import com.formbricks.formbrickssdk.model.javascript.EventType import com.formbricks.formbrickssdk.model.javascript.FileUploadData import com.google.gson.JsonParseException -import timber.log.Timber class WebAppInterface(private val callback: WebAppCallback?) { @@ -22,7 +22,7 @@ class WebAppInterface(private val callback: WebAppCallback?) { */ @JavascriptInterface fun message(data: String) { - Timber.tag("WebAppInterface message").d(data) + Logger.d(data) try { val jsMessage = JsMessageData.from(data) @@ -34,13 +34,13 @@ class WebAppInterface(private val callback: WebAppCallback?) { EventType.ON_FILE_PICK -> { callback?.onFilePick(FileUploadData.from(data)) } } } catch (e: Exception) { - Timber.tag("WebAppInterface error").e(e) + Logger.e(e.message) } catch (e: JsonParseException) { - Timber.tag("WebAppInterface error").e(e, "Failed to parse JSON message: $data") + Logger.e("Failed to parse JSON message: $data") } catch (e: IllegalArgumentException) { - Timber.tag("WebAppInterface error").e(e, "Invalid message format: $data") + Logger.e("Invalid message format: $data") } catch (e: Exception) { - Timber.tag("WebAppInterface error").e(e, "Unexpected error processing message: $data") + Logger.e("Unexpected error processing message: $data") } } diff --git a/packages/android/gradle/libs.versions.toml b/packages/android/gradle/libs.versions.toml index ef49a3c8d3..d5baec625b 100644 --- a/packages/android/gradle/libs.versions.toml +++ b/packages/android/gradle/libs.versions.toml @@ -27,8 +27,6 @@ lifecycleViewmodelKtx = "2.8.7" fragmentKtx = "1.8.5" databindingCommon = "8.8.0" -timber = "5.0.1" - [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -53,7 +51,6 @@ retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", ve retrofit-converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" } okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp3" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } -timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "legacySupportV4" } From 3dea241d7ad5ad4186f2ec2fd1a9987e3ec15013 Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Mon, 17 Mar 2025 06:46:54 -0700 Subject: [PATCH 053/411] docs: tweak docs for sso (#4974) --- docs/mint.json | 4 +- .../configuration/auth-sso/azure-ad-oauth.mdx | 109 +++++++++ .../configuration/auth-sso/google-oauth.mdx | 81 +++++++ .../configuration/auth-sso/oauth.mdx | 208 ------------------ .../auth-sso/open-id-connect.mdx | 45 ++++ .../configuration/auth-sso/saml-sso.mdx | 37 +++- 6 files changed, 266 insertions(+), 218 deletions(-) create mode 100644 docs/self-hosting/configuration/auth-sso/azure-ad-oauth.mdx create mode 100644 docs/self-hosting/configuration/auth-sso/google-oauth.mdx delete mode 100644 docs/self-hosting/configuration/auth-sso/oauth.mdx create mode 100644 docs/self-hosting/configuration/auth-sso/open-id-connect.mdx diff --git a/docs/mint.json b/docs/mint.json index 3e5b1e2ffa..562973b528 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -262,7 +262,9 @@ "group": "Auth & SSO", "icon": "lock", "pages": [ - "self-hosting/configuration/auth-sso/oauth", + "self-hosting/configuration/auth-sso/open-id-connect", + "self-hosting/configuration/auth-sso/azure-ad-oauth", + "self-hosting/configuration/auth-sso/google-oauth", "self-hosting/configuration/auth-sso/saml-sso" ] }, diff --git a/docs/self-hosting/configuration/auth-sso/azure-ad-oauth.mdx b/docs/self-hosting/configuration/auth-sso/azure-ad-oauth.mdx new file mode 100644 index 0000000000..c3bfbafaa1 --- /dev/null +++ b/docs/self-hosting/configuration/auth-sso/azure-ad-oauth.mdx @@ -0,0 +1,109 @@ +--- +title: Azure AD OAuth +description: "Configure Microsoft Entra ID (Azure AD) OAuth for secure Single Sign-On with your Formbricks instance. Use enterprise-grade authentication for your survey platform." +icon: "microsoft" +--- + + + Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license). + + +### Microsoft Entra ID + +Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks instance to allow users to log in using their existing Microsoft credentials. This guide will walk you through the process of setting up an Application Registration for your Formbricks instance. + +### Requirements + +- A Microsoft Entra ID Tenant populated with users. [Create a tenant as per Microsoft's documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant). + +- A Formbricks instance running and accessible. + +- The callback URI for your Formbricks instance: `{WEBAPP_URL}/api/auth/callback/azure-ad` + +## How to connect your Formbricks instance to Microsoft Entra + + + + - Login to the [Microsoft Entra admin center](https://entra.microsoft.com/). + - Go to **Applications** > **App registrations** in the left menu. + + ![first](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250153/image_tobdth.jpg) + + + + - Click the **New registration** button at the top. + + ![second](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250228/image_dmz75t.jpg) + + + + - Name your application something descriptive, such as `Formbricks SSO`. + + ![third](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250292/image_rooa3w.jpg) + + - If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_. + + ![fourth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250542/image_nyndzo.jpg) + + - Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above). + + ![fifth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250776/image_s3pgb6.jpg) + + - Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created. + + + + - On the _Overview_ page, under **Essentials**: + - Copy the entry for **Application (client) ID** to populate the `AZUREAD_CLIENT_ID` variable. + - Copy the entry for **Directory (tenant) ID** to populate the `AZUREAD_TENANT_ID` variable. + + ![sixth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250876/image_dj2vi5.jpg) + + + + - From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**. + + ![seventh](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250913/image_p4zknw.jpg) + + - Make sure you have the **Client secrets** tab active, and click **New client secret**. + + ![eighth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250973/image_kyjray.jpg) + + - Enter a **Description**, set an **Expires** period, then click **Add**. + + + You will need to create a new client secret using these steps whenever your chosen expiry period ends. + + + ![ninth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251467/image_bkirq4.jpg) + + - Copy the entry under **Value** to populate the `AZUREAD_CLIENT_SECRET` variable. + + + Microsoft will only show this value to you immediately after creation, and you will not be able to access it again. If you lose it, simply create a new secret. + + + ![tenth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251234/image_jen6tp.jpg) + + + + - Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container. + + + You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g., "THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters. + + + An example `.env` for Microsoft Entra ID in Formbricks would look like this: + + ```yml Formbricks Env for Microsoft Entra ID SSO + AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c + AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885 + AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3" + ``` + + + + - Restart your Formbricks instance. + - You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant. + + \ No newline at end of file diff --git a/docs/self-hosting/configuration/auth-sso/google-oauth.mdx b/docs/self-hosting/configuration/auth-sso/google-oauth.mdx new file mode 100644 index 0000000000..fef8286e28 --- /dev/null +++ b/docs/self-hosting/configuration/auth-sso/google-oauth.mdx @@ -0,0 +1,81 @@ +--- +title: "Google OAuth" +description: "Configure Google OAuth for secure Single Sign-On with your Formbricks instance. Implement enterprise-grade authentication for your survey platform with Google credentials." +icon: "google" +--- + + + Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license). + + +### Google OAuth + +Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance. + +### Requirements + +- A Google Cloud Platform (GCP) account + +- A Formbricks instance running + +### How to connect your Formbricks instance to Google + + + + - Navigate to the [GCP Console](https://console.cloud.google.com/). + - From the projects list, select a project or create a new one. + + + + - If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**. + - On the left, click **Credentials**. + - Click **Create Credentials**, then select **OAuth client ID**. + + + + - If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**. + - Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted. + + + + - Select the application type **Web application** for your project and enter any additional information required. + - Ensure to specify authorized JavaScript origins and authorized redirect URIs. + + ``` + Authorized JavaScript origins: {WEBAPP_URL} + Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google + ``` + + + + - To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container. + + - In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform: + + ```sh + GOOGLE_CLIENT_ID=your-client-id-here + GOOGLE_CLIENT_SECRET=your-client-secret-here + ``` + + - Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID): + + ```sh + docker exec -it container_id /bin/bash + export GOOGLE_CLIENT_ID=your-client-id-here + export GOOGLE_CLIENT_SECRET=your-client-secret-here + exit + ``` + + + + + Restarting your Docker containers may cause a brief period of downtime. Plan accordingly. + + + - Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. + + - Navigate to your Docker setup directory where your `docker-compose.yml` file is located. + + - Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration. + + diff --git a/docs/self-hosting/configuration/auth-sso/oauth.mdx b/docs/self-hosting/configuration/auth-sso/oauth.mdx deleted file mode 100644 index c2b46b5bc1..0000000000 --- a/docs/self-hosting/configuration/auth-sso/oauth.mdx +++ /dev/null @@ -1,208 +0,0 @@ ---- -title: OAuth -description: "OAuth for Formbricks" -icon: "key" ---- - - - Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Entra ID, Github and OpenID Connect, requires a valid Formbricks Enterprise License. - - -### Google OAuth - -Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance. - -#### Requirements: - -- A Google Cloud Platform (GCP) account. - -- A Formbricks instance running and accessible. - -#### Steps: - -1. **Create a GCP Project**: - - - Navigate to the [GCP Console](https://console.cloud.google.com/). - - - From the projects list, select a project or create a new one. - -2. **Setting up OAuth 2.0**: - - - If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**. - - - On the left, click **Credentials**. - - - Click **Create Credentials**, then select **OAuth client ID**. - -3. **Configure OAuth Consent Screen**: - - - If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**. - - - Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted. - -4. **Create OAuth 2.0 Client IDs**: - - - Select the application type **Web application** for your project and enter any additional information required. - - - Ensure to specify authorized JavaScript origins and authorized redirect URIs. - -```{{ Redirect & Origin URLs -Authorized JavaScript origins: {WEBAPP_URL} -Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google -``` - -- **Update Environment Variables in Docker**: - - - To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container. - - - In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform: - - - Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID): - - ```sh Shell commands - docker exec -it container_id /bin/bash - export GOOGLE_CLIENT_ID=your-client-id-here - export GOOGLE_CLIENT_SECRET=your-client-secret-here - exit - ``` - -```sh env file -GOOGLE_CLIENT_ID=your-client-id-here -GOOGLE_CLIENT_SECRET=your-client-secret-here -``` - -1. **Restart Your Formbricks Instance**: - - - **Note:** Restarting your Docker containers may cause a brief period of downtime. Plan accordingly. - - - Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. Here's how you can do it: - - - Navigate to your Docker setup directory where your `docker-compose.yml` file is located. - - - Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration: - -### Microsoft Entra ID (Azure Active Directory) SSO OAuth - -Do you have a Microsoft Entra ID Tenant? Integrate it with your Formbricks instance to allow users to log in using their existing Microsoft credentials. This guide will walk you through the process of setting up an Application Registration for your Formbricks instance. - -#### Requirements - -- A Microsoft Entra ID Tenant populated with users. [Create a tenant as per Microsoft's documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant). - -- A Formbricks instance running and accessible. - -- The callback URI for your Formbricks instance: `{WEBAPP_URL}/api/auth/callback/azure-ad` - -#### Creating an App Registration - -- Login to the [Microsoft Entra admin center](https://entra.microsoft.com/). - -- Go to **Applications** > **App registrations** in the left menu. - -![first](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250153/image_tobdth.jpg) - -- Click the **New registration** button at the top. - -![second](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250228/image_dmz75t.jpg) - -- Name your application something descriptive, such as `Formbricks SSO`. - -![third](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250292/image_rooa3w.jpg) - -- If you have multiple tenants/organizations, choose the appropriate **Supported account types** option. Otherwise, leave the default option for _Single Tenant_. - -![fourth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250542/image_nyndzo.jpg) - -- Under **Redirect URI**, select **Web** for the platform and paste your Formbricks callback URI (see Requirements above). - -![fifth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250776/image_s3pgb6.jpg) - -- Click **Register** to create the App registration. You will be redirected to your new app's _Overview_ page after it is created. - -- On the _Overview_ page, under **Essentials**: - -- Copy the entry for **Application (client) ID** to populate the `AZUREAD_CLIENT_ID` variable. - -- Copy the entry for **Directory (tenant) ID** to populate the `AZUREAD_TENANT_ID` variable. - -![sixth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250876/image_dj2vi5.jpg) - -- From your App registration's _Overview_ page, go to **Manage** > **Certificates & secrets**. - -![seventh](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250913/image_p4zknw.jpg) - -- Make sure you have the **Client secrets** tab active, and click **New client secret**. - -![eighth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738250973/image_kyjray.jpg) - -- Enter a **Description**, set an **Expires** period, then click **Add**. - - - You will need to create a new client secret using these steps whenever your chosen expiry period ends. - - -![ninth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251467/image_bkirq4.jpg) - -- Copy the entry under **Value** to populate the `AZUREAD_CLIENT_SECRET` variable. - - - Microsoft will only show this value to you immediately after creation, and you will not be able to access it again. If you lose it, simply start from step 9 to create a new secret. - - -![tenth](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738251234/image_jen6tp.jpg) - -- Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container. - - - You must wrap the `AZUREAD_CLIENT_SECRET` value in double quotes (e.g., "THis~iS4faKe.53CreTvALu3"`) to prevent issues with special characters. - - -An example `.env` for Microsoft Entra ID in Formbricks would look like: - -```yml Formbricks Env for Microsoft Entra ID SSO -AZUREAD_CLIENT_ID=a25cadbd-f049-4690-ada3-56a163a72f4c -AZUREAD_TENANT_ID=2746c29a-a3a6-4ea1-8762-37816d4b7885 -AZUREAD_CLIENT_SECRET="THis~iS4faKe.53CreTvALu3" -``` - -- Restart your Formbricks instance. - -- You're all set! Users can now sign up & log in using their Microsoft credentials associated with your Entra ID Tenant. - -## OpenID Configuration - -Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance. - -- Configure your OIDC provider & get the following variables: - -- `OIDC_CLIENT_ID` - -- `OIDC_CLIENT_SECRET` - -- `OIDC_ISSUER` - -- `OIDC_SIGNING_ALGORITHM` - - - Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`. - - -- Update these environment variables in your `docker-compose.yml` or pass it directly to the running container. - -An example configuration for a FusionAuth OpenID Connect in Formbricks would look like: - - -```yml Formbricks Env for FusionAuth OIDC -OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7 -OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s -OIDC_ISSUER=http://localhost:9011 -OIDC_DISPLAY_NAME=FusionAuth -OIDC_SIGNING_ALGORITHM=HS256 -``` - - -- Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider. - -- Restart your Formbricks instance. - -- You're all set! Users can now sign up & log in using their OIDC credentials. diff --git a/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx b/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx new file mode 100644 index 0000000000..0202467879 --- /dev/null +++ b/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx @@ -0,0 +1,45 @@ +--- +title: "Open ID Connect" +description: "Configure Open ID Connect for secure Single Sign-On with your Formbricks instance. Implement enterprise-grade authentication for your survey platform with Open ID Connect." +icon: "key" +--- + + + Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Azure AD, and OpenID Connect, requires is part of the [Enterprise Edition](/self-hosting/advanced/license). + + +Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance. + +- Configure your OIDC provider & get the following variables: + +- `OIDC_CLIENT_ID` + +- `OIDC_CLIENT_SECRET` + +- `OIDC_ISSUER` + +- `OIDC_SIGNING_ALGORITHM` + + + Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`. + + +- Update these environment variables in your `docker-compose.yml` or pass it directly to the running container. + +An example configuration for a FusionAuth OpenID Connect in Formbricks would look like: + + +```yml Formbricks Env for FusionAuth OIDC +OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7 +OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s +OIDC_ISSUER=http://localhost:9011 +OIDC_DISPLAY_NAME=FusionAuth +OIDC_SIGNING_ALGORITHM=HS256 +``` + + +- Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider. + +- Restart your Formbricks instance. + +- You're all set! Users can now sign up & log in using their OIDC credentials. diff --git a/docs/self-hosting/configuration/auth-sso/saml-sso.mdx b/docs/self-hosting/configuration/auth-sso/saml-sso.mdx index 619fea7e19..c100f2f64e 100644 --- a/docs/self-hosting/configuration/auth-sso/saml-sso.mdx +++ b/docs/self-hosting/configuration/auth-sso/saml-sso.mdx @@ -1,7 +1,7 @@ --- title: "SAML SSO" icon: "user-shield" -description: "How to set up SAML SSO for Formbricks" +description: "Configure SAML Single Sign-On (SSO) for secure enterprise authentication with your Formbricks instance." --- You require an Enterprise License along with a SAML SSO add-on to avail this feature. @@ -12,7 +12,7 @@ Formbricks supports SAML Single Sign-On (SSO) to enable secure, centralized auth To learn more about SAML Jackson, please refer to the [BoxyHQ SAML Jackson documentation](https://boxyhq.com/docs/jackson/deploy). -## How SAML Works in Formbricks +## How SAML works in Formbricks SAML (Security Assertion Markup Language) is an XML-based standard for exchanging authentication and authorization data between an Identity Provider (IdP) and Formbricks. Here's how the integration works with BoxyHQ Jackson embedded into the flow: @@ -37,7 +37,7 @@ SAML (Security Assertion Markup Language) is an XML-based standard for exchangin 7. **Access Granted:** Formbricks logs the user in using the verified information. -## SAML Authentication Flow Sequence Diagram +## SAML Auth Flow Sequence Diagram Below is a sequence diagram illustrating the complete SAML authentication flow with BoxyHQ Jackson integrated: @@ -67,12 +67,31 @@ sequenceDiagram To configure SAML SSO in Formbricks, follow these steps: -1. **Database Setup:** Configure a dedicated database for SAML by setting the `SAML_DATABASE_URL` environment variable in your `docker-compose.yml` file (e.g., `postgres://postgres:postgres@postgres:5432/formbricks-saml`). If you're using a self-signed certificate for Postgres, include the `sslmode=disable` parameter. -2. **IdP Application:** Create a SAML application in your IdP by following your provider's instructions([SAML Setup](/development/guides/auth-and-provision/setup-saml-with-identity-providers)) -3. **User Provisioning:** Provision users in your IdP and configure access to the IdP SAML app for all your users (who need access to Formbricks). -4. **Metadata:** Keep the XML metadata from your IdP handy for the next step. -5. **Metadata Setup:** Create a file called `connection.xml` in your self-hosted Formbricks instance's `formbricks/saml-connection` directory and paste the XML metadata from your IdP into it. Please create the directory if it doesn't exist. Your metadata file should start with a tag like this: `<...>` or ``. Please remove any extra text from the metadata. -6. **Restart Formbricks:** Restart Formbricks to apply the changes. You can do this by running `docker compose down` and then `docker compose up -d`. + + + Configure a dedicated database for SAML by setting the `SAML_DATABASE_URL` environment variable in your `docker-compose.yml` file (e.g., `postgres://postgres:postgres@postgres:5432/formbricks-saml`). If you're using a self-signed certificate for Postgres, include the `sslmode=disable` parameter. + + + + Create a SAML application in your IdP by following your provider's instructions([SAML Setup](/development/guides/auth-and-provision/setup-saml-with-identity-providers)) + + + + Provision users in your IdP and configure access to the IdP SAML app for all your users (who need access to Formbricks). + + + + Keep the XML metadata from your IdP handy for the next step. + + + + Create a file called `connection.xml` in your self-hosted Formbricks instance's `formbricks/saml-connection` directory and paste the XML metadata from your IdP into it. Please create the directory if it doesn't exist. Your metadata file should start with a tag like this: `<...>` or ``. Please remove any extra text from the metadata. + + + + Restart Formbricks to apply the changes. You can do this by running `docker compose down` and then `docker compose up -d`. + + We don't support multiple SAML connections yet. You can only have one SAML connection at a time. If you From 7971681d0278546ca9e0cbacda1dc5a8e041349a Mon Sep 17 00:00:00 2001 From: Harsh Shrikant Bhat <90265455+harshsbhat@users.noreply.github.com> Date: Mon, 17 Mar 2025 22:20:17 +0530 Subject: [PATCH 054/411] docs: Remove duplicate titles for better SEO (#4962) --- docs/self-hosting/configuration/auth-sso/saml-sso.mdx | 2 +- docs/xm-and-surveys/core-features/integrations/overview.mdx | 2 +- docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx | 2 +- docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/self-hosting/configuration/auth-sso/saml-sso.mdx b/docs/self-hosting/configuration/auth-sso/saml-sso.mdx index c100f2f64e..39db134cab 100644 --- a/docs/self-hosting/configuration/auth-sso/saml-sso.mdx +++ b/docs/self-hosting/configuration/auth-sso/saml-sso.mdx @@ -1,5 +1,5 @@ --- -title: "SAML SSO" +title: "SAML SSO - Self-hosted" icon: "user-shield" description: "Configure SAML Single Sign-On (SSO) for secure enterprise authentication with your Formbricks instance." --- diff --git a/docs/xm-and-surveys/core-features/integrations/overview.mdx b/docs/xm-and-surveys/core-features/integrations/overview.mdx index 0d8ee9ac0d..40889dfb19 100644 --- a/docs/xm-and-surveys/core-features/integrations/overview.mdx +++ b/docs/xm-and-surveys/core-features/integrations/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Overview" +title: "Third-party Integrations" description: "Configure third-party integrations with Formbricks Cloud." --- diff --git a/docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx b/docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx index c1e689523d..b59bbd7650 100644 --- a/docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx +++ b/docs/xm-and-surveys/surveys/link-surveys/quickstart.mdx @@ -1,5 +1,5 @@ --- -title: "Quickstart" +title: "Quickstart - Link Surveys" description: "Create your first link survey in under 5 minutes." icon: "rocket" --- diff --git a/docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx index 09ba407551..04695738d0 100644 --- a/docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx +++ b/docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx @@ -1,5 +1,5 @@ --- -title: "Quickstart" +title: "Quickstart - Web & App Surveys" description: "App surveys deliver 6–10x higher conversion rates compared to email surveys. If you are new to Formbricks, follow the steps in this guide to launch a survey in your web or mobile app (React Native) within 10–15 minutes." icon: "rocket" --- From 625a4dcfaefeb9e0f55538c05a24ce3da69bce99 Mon Sep 17 00:00:00 2001 From: Jakob Schott <154420406+jakobsitory@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:51:43 +0100 Subject: [PATCH 055/411] fix: changed 'Download example CSV'-link to a button (#4975) --- .../contacts/components/upload-contacts-button.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx b/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx index fcf4381dcb..829caf3752 100644 --- a/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx +++ b/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx @@ -360,13 +360,11 @@ export const UploadContactsCSVButton = ({ )}

{!csvResponse.length && ( -

- - {t("environments.contacts.upload_contacts_modal_download_example_csv")}{" "} - -

+
+ +
)}
From 39aa9f0941d3d53ca653d302c3846fc571194fdd Mon Sep 17 00:00:00 2001 From: Piyush Jain <122745947+d3vb0ox@users.noreply.github.com> Date: Mon, 17 Mar 2025 23:15:32 +0530 Subject: [PATCH 056/411] chore(infra-updates): updates and fixes (#4976) --- .../workflows/terrafrom-plan-and-apply.yml | 30 +--- helm-chart/templates/cronjob.yaml | 164 +++++++++++------- helm-chart/templates/deployment.yaml | 13 +- helm-chart/values.yaml | 6 +- infra/terraform/clouwatch.tf | 27 +++ infra/terraform/main.tf | 76 +++++++- 6 files changed, 213 insertions(+), 103 deletions(-) create mode 100644 infra/terraform/clouwatch.tf diff --git a/.github/workflows/terrafrom-plan-and-apply.yml b/.github/workflows/terrafrom-plan-and-apply.yml index 25b1422d0f..a2f0bc2107 100644 --- a/.github/workflows/terrafrom-plan-and-apply.yml +++ b/.github/workflows/terrafrom-plan-and-apply.yml @@ -2,12 +2,12 @@ name: 'Terraform' on: workflow_dispatch: - pull_request: push: branches: - main - paths: - - 'infra/terraform/**' + pull_request: + branches: + - main permissions: id-token: write @@ -37,40 +37,16 @@ jobs: continue-on-error: true working-directory: infra/terraform -# - name: Post Format -# if: always() && github.ref != 'refs/heads/main' && (steps.fmt.outcome == 'success' || steps.fmt.outcome == 'failure') -# uses: robburger/terraform-pr-commenter@v1 -# with: -# commenter_type: fmt -# commenter_input: ${{ format('{0}{1}', steps.fmt.outputs.stdout, steps.fmt.outputs.stderr) }} -# commenter_exitcode: ${{ steps.fmt.outputs.exitcode }} - - name: Terraform Init id: init run: terraform init working-directory: infra/terraform -# - name: Post Init -# if: always() && github.ref != 'refs/heads/main' && (steps.init.outcome == 'success' || steps.init.outcome == 'failure') -# uses: robburger/terraform-pr-commenter@v1 -# with: -# commenter_type: init -# commenter_input: ${{ format('{0}{1}', steps.init.outputs.stdout, steps.init.outputs.stderr) }} -# commenter_exitcode: ${{ steps.init.outputs.exitcode }} - - name: Terraform Validate id: validate run: terraform validate working-directory: infra/terraform -# - name: Post Validate -# if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure') -# uses: robburger/terraform-pr-commenter@v1 -# with: -# commenter_type: validate -# commenter_input: ${{ format('{0}{1}', steps.validate.outputs.stdout, steps.validate.outputs.stderr) }} -# commenter_exitcode: ${{ steps.validate.outputs.exitcode }} - - name: Terraform Plan id: plan run: terraform plan -out .planfile diff --git a/helm-chart/templates/cronjob.yaml b/helm-chart/templates/cronjob.yaml index b6051079ea..6c3cbbe46f 100644 --- a/helm-chart/templates/cronjob.yaml +++ b/helm-chart/templates/cronjob.yaml @@ -1,49 +1,40 @@ {{- if (.Values.cronJob).enabled }} {{- range $name, $job := .Values.cronJob.jobs }} --- -apiVersion: {{ if $.Capabilities.APIVersions.Has "batch/v1/CronJob" }}batch/v1{{ else }}batch/v1beta1{{ end }} +{{ if $.Capabilities.APIVersions.Has "batch/v1/CronJob" -}} +apiVersion: batch/v1 +{{- else -}} +apiVersion: batch/v1beta1 +{{- end }} kind: CronJob metadata: - name: {{ $name }} labels: - # Standard labels for tracking CronJobs - {{- include "formbricks.labels" $ | nindent 4 }} - - # Additional labels if specified - {{- if $job.additionalLabels }} - {{- toYaml $job.additionalLabels | indent 4 }} - {{- end }} - - # Additional annotations if specified - {{- if $job.annotations }} + {{- include "formbricks.labels" $ | nindent 4 }} +{{- if $job.additionalLabels }} +{{ $job.additionalLabels | indent 4 }} +{{- end }} +{{- if $job.annotations }} annotations: - {{- toYaml $job.annotations | indent 4 }} - {{- end }} - +{{ $job.annotations | indent 4 }} +{{- end }} + name: {{ $name }} + namespace: {{ template "formbricks.namespace" $ }} spec: - # Define the execution schedule for the job schedule: {{ $job.schedule | quote }} - - # Kubernetes 1.27+ supports time zones for CronJobs - {{- if ge (int $.Capabilities.KubeVersion.Minor) 27 }} - {{- if $job.timeZone }} +{{- if ge (int $.Capabilities.KubeVersion.Minor) 27 }} +{{- if $job.timeZone }} timeZone: {{ $job.timeZone }} - {{- end }} - {{- end }} - - # Define job retention policies - {{- if $job.successfulJobsHistoryLimit }} +{{ end }} +{{- end }} +{{- if $job.successfulJobsHistoryLimit }} successfulJobsHistoryLimit: {{ $job.successfulJobsHistoryLimit }} - {{- end }} - {{- if $job.failedJobsHistoryLimit }} - failedJobsHistoryLimit: {{ $job.failedJobsHistoryLimit }} - {{- end }} - - # Define concurrency policy - {{- if $job.concurrencyPolicy }} +{{ end }} +{{- if $job.concurrencyPolicy }} concurrencyPolicy: {{ $job.concurrencyPolicy }} - {{- end }} - +{{ end }} +{{- if $job.failedJobsHistoryLimit }} + failedJobsHistoryLimit: {{ $job.failedJobsHistoryLimit }} +{{ end }} jobTemplate: spec: {{- with $job.activeDeadlineSeconds }} @@ -55,48 +46,101 @@ spec: template: metadata: labels: - {{- include "formbricks.labels" $ | nindent 12 }} - - # Additional pod-level labels + {{- include "formbricks.labels" $ | nindent 12 }} {{- with $job.additionalPodLabels }} {{- toYaml . | nindent 12 }} {{- end }} - - # Additional annotations {{- with $job.additionalPodAnnotations }} - annotations: {{- toYaml . | nindent 12 }} + annotations: {{ toYaml . | nindent 12 }} {{- end }} - spec: - # Define the service account if RBAC is enabled {{- if $.Values.rbac.enabled }} + {{- if $.Values.rbac.serviceAccount.name }} + serviceAccountName: {{ $.Values.rbac.serviceAccount.name }} + {{- else }} serviceAccountName: {{ template "formbricks.name" $ }} {{- end }} - - # Define the job container + {{- end }} containers: - - name: {{ $name }} - image: "{{ required "Image repository is undefined" $job.image.repository }}:{{ $job.image.tag | default "latest" }}" - imagePullPolicy: {{ $job.image.imagePullPolicy | default "IfNotPresent" }} - - # Environment variables from values + - name: {{ $name }} + {{- $image := required (print "Undefined image repo for container '" $name "'") $job.image.repository }} + {{- with $job.image.tag }} {{- $image = print $image ":" . }} {{- end }} + {{- with $job.image.digest }} {{- $image = print $image "@" . }} {{- end }} + image: {{ $image }} + {{- if $job.image.imagePullPolicy }} + imagePullPolicy: {{ $job.image.imagePullPolicy }} + {{ end }} {{- with $job.env }} - env: + env: {{- range $key, $value := $job.env }} - - name: {{ $key }} - value: {{ $value | quote }} + - name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }} + {{- if kindIs "string" $value }} + value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }} + {{- else }} + {{- toYaml $value | nindent 16 }} + {{- end }} {{- end }} {{- end }} - - # Define command and arguments if specified - {{- with $job.command }} - command: {{- toYaml . | indent 14 }} + {{- with $job.envFrom }} + envFrom: + {{ toYaml . | indent 12 }} + {{- end }} + {{- if $job.command }} + command: {{ $job.command }} {{- end }} - {{- with $job.args }} - args: {{- toYaml . | indent 14 }} + args: + {{- range . }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- with $job.resources }} + resources: + {{ toYaml . | indent 14 }} {{- end }} - - restartPolicy: {{ $job.restartPolicy | default "OnFailure" }} + {{- with $job.volumeMounts }} + volumeMounts: +{{ toYaml . | indent 12 }} + {{- end }} + {{- with $job.securityContext }} + securityContext: {{ toYaml . | nindent 14 }} + {{- end }} + {{- with $job.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 12 }} + {{- end }} + {{- with $job.affinity }} + affinity: +{{ toYaml . | indent 12 }} + {{- end }} + {{- with $job.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- with $job.tolerations }} + tolerations: {{ toYaml . | nindent 12 }} + {{- end }} + {{- with $job.topologySpreadConstraints }} + topologySpreadConstraints: {{ toYaml . | nindent 12 }} + {{- end }} + {{- if $job.restartPolicy }} + restartPolicy: {{ $job.restartPolicy }} + {{ else }} + restartPolicy: OnFailure + {{ end }} + {{- with $job.imagePullSecrets }} + imagePullSecrets: +{{ toYaml . | indent 12 }} + {{ end }} + {{- if $job.dnsConfig }} + dnsConfig: +{{ toYaml $job.dnsConfig | indent 12 }} + {{- end }} + {{- if $job.dnsPolicy }} + dnsPolicy: {{ $job.dnsPolicy }} + {{- end }} + {{- with $job.volumes }} + volumes: +{{ toYaml . | indent 12 }} + {{- end }} {{- end }} {{- end }} diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml index 378233d183..a9fe7fd64e 100644 --- a/helm-chart/templates/deployment.yaml +++ b/helm-chart/templates/deployment.yaml @@ -13,6 +13,9 @@ metadata: {{- if .Values.deployment.annotations }} {{- toYaml .Values.deployment.annotations | nindent 4 }} {{- end }} + {{- if .Values.deployment.reloadOnChange }} + reloader.stakater.com/auto: "true" + {{- end }} {{- end }} spec: {{- if .Values.deployment.replicas }} @@ -124,13 +127,13 @@ spec: {{- end }} {{- end }} env: - {{- if and (.Values.enterprise.enabled) (ne .Values.enterprise.licenseKey "") }} - - name: ENTERPRISE_LICENSE_KEY - value: {{ .Values.enterprise.licenseKey | quote }} - {{- end }} {{- range $key, $value := .Values.deployment.env }} - name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }} - {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | indent 10 }} + {{- if kindIs "string" $value }} + value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }} + {{- else }} + {{- toYaml $value | nindent 14 }} + {{- end }} {{- end }} {{- if .Values.deployment.resources }} resources: diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 684b3ec130..def45c5aa5 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -60,10 +60,8 @@ deployment: # Environment variables passed to the app container env: - EMAIL_VERIFICATION_DISABLED: - value: "1" - PASSWORD_RESET_DISABLED: - value: "1" + DOCKER_CRON_ENABLED: + value: "0" # Tolerations for scheduling pods on tainted nodes tolerations: [] diff --git a/infra/terraform/clouwatch.tf b/infra/terraform/clouwatch.tf new file mode 100644 index 0000000000..2dc70cf68d --- /dev/null +++ b/infra/terraform/clouwatch.tf @@ -0,0 +1,27 @@ +data "aws_ssm_parameter" "slack_notification_channel" { + name = "/prod/formbricks/slack-webhook-url" + with_decryption = true +} + +resource "aws_cloudwatch_log_group" "cloudwatch_cis_benchmark" { + name = "/aws/cis-benchmark-group" + retention_in_days = 365 +} + +module "notify-slack" { + source = "terraform-aws-modules/notify-slack/aws" + version = "6.6.0" + + slack_channel = "kubernetes" + slack_username = "formbricks-cloudwatch" + slack_webhook_url = data.aws_ssm_parameter.slack_notification_channel.value + sns_topic_name = "cloudwatch-alarms" + create_sns_topic = true +} + +module "cloudwatch_cis-alarms" { + source = "terraform-aws-modules/cloudwatch/aws//modules/cis-alarms" + version = "5.7.1" + log_group_name = aws_cloudwatch_log_group.cloudwatch_cis_benchmark.name + alarm_actions = [module.notify-slack.slack_topic_arn] +} diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf index 1f020e7f5c..7cb6c8ca86 100644 --- a/infra/terraform/main.tf +++ b/infra/terraform/main.tf @@ -32,11 +32,6 @@ module "route53_zones" { } } -output "route53_ns_records" { - value = module.route53_zones.route53_zone_name_servers -} - - module "acm" { source = "terraform-aws-modules/acm/aws" version = "5.1.1" @@ -641,6 +636,7 @@ resource "helm_release" "formbricks" { eks.amazonaws.com/role-arn: ${module.formkey-aws-access.iam_role_arn} serviceMonitor: enabled: true + reloadOnChange: true deployment: image: repository: "ghcr.io/formbricks/formbricks-experimental" @@ -656,13 +652,13 @@ resource "helm_release" "formbricks" { type: secret nameSuffix: app-env annotations: - deployed_at: ${timestamp()} + last_updated_at: ${timestamp()} externalSecret: enabled: true # Enable/disable ExternalSecrets secretStore: name: aws-secrets-manager kind: ClusterSecretStore - refreshInterval: "1h" + refreshInterval: "1m" files: app-env: dataFrom: @@ -670,6 +666,72 @@ resource "helm_release" "formbricks" { app-secrets: dataFrom: key: "prod/formbricks/secrets" + cronJob: + enabled: true + jobs: + survey-status: + schedule: "0 0 * * *" + env: + CRON_SECRET: + valueFrom: + secretKeyRef: + name: "formbricks-app-env" + key: "CRON_SECRET" + WEBAPP_URL: + valueFrom: + secretKeyRef: + name: "formbricks-app-env" + key: "WEBAPP_URL" + image: + repository: curlimages/curl + tag: latest + imagePullPolicy: IfNotPresent + args: + - "/bin/sh" + - "-c" + - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" "$WEBAPP_URL/api/cron/survey-status"' + weekely-summary: + schedule: "0 8 * * 1" + env: + CRON_SECRET: + valueFrom: + secretKeyRef: + name: "formbricks-app-env" + key: "CRON_SECRET" + WEBAPP_URL: + valueFrom: + secretKeyRef: + name: "formbricks-app-env" + key: "WEBAPP_URL" + image: + repository: curlimages/curl + tag: latest + imagePullPolicy: IfNotPresent + args: + - "/bin/sh" + - "-c" + - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" "$WEBAPP_URL/api/cron/weekly-summary"' + ping: + schedule: "0 9 * * *" + env: + CRON_SECRET: + valueFrom: + secretKeyRef: + name: "formbricks-app-env" + key: "CRON_SECRET" + WEBAPP_URL: + valueFrom: + secretKeyRef: + name: "formbricks-app-env" + key: "WEBAPP_URL" + image: + repository: curlimages/curl + tag: latest + imagePullPolicy: IfNotPresent + args: + - "/bin/sh" + - "-c" + - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" "$WEBAPP_URL/api/cron/ping"' EOT ] } From 6a123a2399ffc0b818b333d3cb9f89ebd20513ef Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Tue, 18 Mar 2025 03:23:10 -0700 Subject: [PATCH 057/411] fix: Harden GitHub Actions (#4982) Signed-off-by: StepSecurity Bot --- .github/workflows/cron-surveyStatusUpdate.yml | 2 +- .github/workflows/e2e.yml | 2 +- .github/workflows/release-helm-chart.yml | 14 +++++++++++--- .github/workflows/terrafrom-plan-and-apply.yml | 13 +++++++++---- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cron-surveyStatusUpdate.yml b/.github/workflows/cron-surveyStatusUpdate.yml index 1e92f2f5aa..7563067db5 100644 --- a/.github/workflows/cron-surveyStatusUpdate.yml +++ b/.github/workflows/cron-surveyStatusUpdate.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 with: egress-policy: audit diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 16ab4a2459..9b631798f3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -142,7 +142,7 @@ jobs: path: playwright-report/ retention-days: 30 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: failure() with: name: app-logs diff --git a/.github/workflows/release-helm-chart.yml b/.github/workflows/release-helm-chart.yml index 4cb25c900b..85cfe16894 100644 --- a/.github/workflows/release-helm-chart.yml +++ b/.github/workflows/release-helm-chart.yml @@ -5,6 +5,9 @@ on: types: - published +permissions: + contents: read + jobs: publish: runs-on: ubuntu-latest @@ -12,14 +15,19 @@ jobs: packages: write contents: read steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Extract release version run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV - name: Set up Helm - uses: azure/setup-helm@v3 + uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 with: version: latest @@ -27,7 +35,7 @@ jobs: run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin - name: Install YQ - uses: dcarbone/install-yq-action@v1.3.1 + uses: dcarbone/install-yq-action@4075b4dca348d74bd83f2bf82d30f25d7c54539b # v1.3.1 - name: Update Chart.yaml with new version run: | diff --git a/.github/workflows/terrafrom-plan-and-apply.yml b/.github/workflows/terrafrom-plan-and-apply.yml index a2f0bc2107..a085710ec5 100644 --- a/.github/workflows/terrafrom-plan-and-apply.yml +++ b/.github/workflows/terrafrom-plan-and-apply.yml @@ -19,17 +19,22 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + with: + egress-policy: audit + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 with: role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }} aws-region: "eu-central-1" - name: Setup Terraform - uses: hashicorp/setup-terraform@v3 + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 - name: Terraform Format id: fmt @@ -53,7 +58,7 @@ jobs: working-directory: infra/terraform - name: Post PR comment - uses: borchero/terraform-plan-comment@v2 + uses: borchero/terraform-plan-comment@3399d8dbae8b05185e815e02361ede2949cd99c4 # v2.4.0 if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure') with: token: ${{ github.token }} From 646fe9c67f3a229ce06ef396b7f0ed9514251c9e Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:43:31 +0530 Subject: [PATCH 058/411] feat: optional cron jobs check (#4966) --- .env.example | 3 +++ apps/web/Dockerfile | 7 ++++++- docker/docker-compose.yml | 3 +++ .../configuration/environment-variables.mdx | 5 +++-- docs/self-hosting/setup/cluster-setup.mdx | 17 +++++++++++++++++ helm-chart/values.yaml | 2 +- packages/lib/env.ts | 2 ++ turbo.json | 1 + 8 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 8564c8edc7..9110d3c192 100644 --- a/.env.example +++ b/.env.example @@ -97,6 +97,9 @@ PASSWORD_RESET_DISABLED=1 # Organization Invite. Disable the ability for invited users to create an account. # INVITE_DISABLED=1 +# Docker cron jobs. Disable the supercronic cron jobs in the Docker image (useful for cluster setups). +# DOCKER_CRON_ENABLED=1 + ########## # Other # ########## diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 2bee6c354f..b16f185c9f 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -111,7 +111,12 @@ VOLUME /home/nextjs/apps/web/uploads/ RUN mkdir -p /home/nextjs/apps/web/saml-connection VOLUME /home/nextjs/apps/web/saml-connection -CMD supercronic -quiet /app/docker/cronjobs & \ +CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \ + echo "Starting cron jobs..."; \ + supercronic -quiet /app/docker/cronjobs & \ + else \ + echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0"; \ + fi; \ (cd packages/database && npm run db:migrate:deploy) && \ (cd packages/database && npm run db:create-saml-database:deploy) && \ exec node apps/web/server.js diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 4e87afb250..a0f2272186 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -69,6 +69,9 @@ x-environment: &environment # Set the below to your Unsplash API Key for their Survey Backgrounds # UNSPLASH_ACCESS_KEY: + # Set the below to 0 to disable cron jobs + # DOCKER_CRON_ENABLED: 1 + ################################################### OPTIONAL (STORAGE) ################################################### # Set the below to set a custom Upload Directory diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index 77f768ce22..7783a9793c 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -59,9 +59,10 @@ These variables are present inside your machine’s docker-compose file. Restart | OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | | | OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 | | OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | | -| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | | +| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | | | CUSTOM_CACHE_DISABLED | Disables custom cache handler if set to 1 (required for deployment on Vercel) | optional | | | PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | | -| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 | | optional | | +| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 | +| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 | Note: If you want to configure something that is not possible via above, please open an issue on our GitHub repo here or reach out to us on Github Discussions and we’ll try our best to work out a solution with you. diff --git a/docs/self-hosting/setup/cluster-setup.mdx b/docs/self-hosting/setup/cluster-setup.mdx index ad2c05f256..eae4fc82ce 100644 --- a/docs/self-hosting/setup/cluster-setup.mdx +++ b/docs/self-hosting/setup/cluster-setup.mdx @@ -160,6 +160,19 @@ When using S3 in a cluster setup, ensure that: - The bucket has appropriate CORS settings configured - IAM roles/users have sufficient permissions for read/write operations +## Disabling Docker Cron Jobs + +When running Formbricks in a cluster setup, you should disable the built-in cron jobs in the Docker image to prevent them from running on multiple instances simultaneously. Instead, you should set up cron jobs in your orchestration system (like Kubernetes) to run on a single instance or as separate jobs. + +To disable the Docker cron jobs, set the following environment variable: + +```sh env +# Disable Docker cron jobs (0 = disabled, 1 = enabled) +DOCKER_CRON_ENABLED=0 +``` + +This will prevent the cron jobs from starting in the Docker container while still allowing all other Formbricks functionality to work normally. + ## Kubernetes Setup Formbricks provides an official Helm chart for deploying the entire cluster stack on Kubernetes. The Helm chart is available in the [Formbricks GitHub repository](https://github.com/formbricks/formbricks/tree/main/helm-chart). @@ -167,6 +180,7 @@ Formbricks provides an official Helm chart for deploying the entire cluster stac ### Features of the Helm Chart The Helm chart provides a complete deployment solution that includes: + - Formbricks application with configurable replicas - PostgreSQL database (with optional HA configuration) - Redis cluster for caching @@ -176,12 +190,14 @@ The Helm chart provides a complete deployment solution that includes: ### Installation Steps 1. Add the Formbricks Helm repository: + ```sh helm repo add formbricks https://raw.githubusercontent.com/formbricks/formbricks/main/helm-chart helm repo update ``` 2. Install the chart: + ```sh helm install formbricks formbricks/formbricks ``` @@ -189,6 +205,7 @@ helm install formbricks formbricks/formbricks ### Configuration Options The Helm chart can be customized using a `values.yaml` file to configure: + - Number of Formbricks replicas - Resource limits and requests - Database configuration diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index def45c5aa5..c33e1469f6 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -296,4 +296,4 @@ postgresql: containerSecurityContext: enabled: true runAsUser: 1001 - readOnlyRootFilesystem: false + readOnlyRootFilesystem: false \ No newline at end of file diff --git a/packages/lib/env.ts b/packages/lib/env.ts index c2b13e147f..08650a2615 100644 --- a/packages/lib/env.ts +++ b/packages/lib/env.ts @@ -22,6 +22,7 @@ export const env = createEnv({ BREVO_LIST_ID: z.string().optional(), DATABASE_URL: z.string().url(), DEBUG: z.enum(["1", "0"]).optional(), + DOCKER_CRON_ENABLED: z.enum(["1", "0"]).optional(), DEFAULT_ORGANIZATION_ID: z.string().optional(), DEFAULT_ORGANIZATION_ROLE: z.enum(["owner", "manager", "member", "billing"]).optional(), E2E_TESTING: z.enum(["1", "0"]).optional(), @@ -153,6 +154,7 @@ export const env = createEnv({ DEBUG: process.env.DEBUG, DEFAULT_ORGANIZATION_ID: process.env.DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_ROLE: process.env.DEFAULT_ORGANIZATION_ROLE, + DOCKER_CRON_ENABLED: process.env.DOCKER_CRON_ENABLED, E2E_TESTING: process.env.E2E_TESTING, EMAIL_AUTH_DISABLED: process.env.EMAIL_AUTH_DISABLED, EMAIL_VERIFICATION_DISABLED: process.env.EMAIL_VERIFICATION_DISABLED, diff --git a/turbo.json b/turbo.json index be2d78dc9a..7a7893accb 100644 --- a/turbo.json +++ b/turbo.json @@ -94,6 +94,7 @@ "BREVO_LIST_ID", "DEFAULT_ORGANIZATION_ID", "DEFAULT_ORGANIZATION_ROLE", + "DOCKER_CRON_ENABLED", "CRON_SECRET", "CUSTOM_CACHE_DISABLED", "DATABASE_URL", From ca5ea315d69689b2cb0e4b7a3919c9d64d84608e Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Tue, 18 Mar 2025 11:49:12 +0100 Subject: [PATCH 059/411] chore: determine formbricks version on release (#4985) --- .github/workflows/prepare-release.yml | 67 --------------------- .github/workflows/release-docker-github.yml | 12 ++++ .github/workflows/release-docker.yml | 19 +++--- apps/web/package.json | 2 +- 4 files changed, 25 insertions(+), 75 deletions(-) delete mode 100644 .github/workflows/prepare-release.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml deleted file mode 100644 index c2ad9307f2..0000000000 --- a/.github/workflows/prepare-release.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Prepare release -run-name: Prepare release ${{ inputs.next_version }} - -on: - workflow_dispatch: - inputs: - next_version: - required: true - type: string - description: "Version name" - -permissions: - contents: write - pull-requests: write - -jobs: - prepare_release: - runs-on: ubuntu-latest - - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 - with: - egress-policy: audit - - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - - uses: ./.github/actions/dangerous-git-checkout - - - name: Configure git - run: | - git config --local user.email "github-actions@github.com" - git config --local user.name "GitHub Actions" - - - name: Setup Node.js 20.x - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af - with: - node-version: 20.x - - - name: Install pnpm - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 - - - name: Install dependencies - run: pnpm install --config.platform=linux --config.architecture=x64 - - - name: Bump version - run: | - cd apps/web - pnpm version ${{ inputs.next_version }} --no-workspaces-update - - - name: Commit changes and create a branch - run: | - branch_name="release-v${{ inputs.next_version }}" - git checkout -b "$branch_name" - git add . - git commit -m "chore: release v${{ inputs.next_version }}" - git push origin "$branch_name" - - - name: Create pull request - run: | - gh pr create \ - --base main \ - --head "release-v${{ inputs.next_version }}" \ - --title "chore: bump version to v${{ inputs.next_version }}" \ - --body "This PR contains the changes for the v${{ inputs.next_version }} release." - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-docker-github.yml b/.github/workflows/release-docker-github.yml index d648ae6760..837dcfe4b5 100644 --- a/.github/workflows/release-docker-github.yml +++ b/.github/workflows/release-docker-github.yml @@ -42,6 +42,18 @@ jobs: - name: Checkout repository uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - name: Get Release Tag + id: extract_release_tag + run: | + TAG=${{ github.ref }} + TAG=${TAG#refs/tags/v} + echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV + + - name: Update package.json version + run: | + sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json + cat ./apps/web/package.json | grep version + - name: Set up Depot CLI uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0 diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index 3f4c3680c7..d3367333ca 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -27,6 +27,18 @@ jobs: - name: Checkout Repo uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0 + - name: Get Release Tag + id: extract_release_tag + run: | + TAG=${{ github.ref }} + TAG=${TAG#refs/tags/v} + echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV + + - name: Update package.json version + run: | + sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json + cat ./apps/web/package.json | grep version + - name: Log in to Docker Hub uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 with: @@ -36,13 +48,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - - name: Get Release Tag - id: extract_release_tag - run: | - TAG=${{ github.ref }} - TAG=${TAG#refs/tags/v} - echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV - - name: Build and push Docker image uses: docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4.2.1 with: diff --git a/apps/web/package.json b/apps/web/package.json index 7a8e0245ef..cd9c7f17d2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@formbricks/web", - "version": "3.4.0", + "version": "0.0.0", "private": true, "scripts": { "clean": "rimraf .turbo node_modules .next", From 07dba906798f0d5d79da553e0484544e693aa4dc Mon Sep 17 00:00:00 2001 From: Peter Pesti-Varga Date: Tue, 18 Mar 2025 13:14:25 +0100 Subject: [PATCH 060/411] fix: Android build fixes (#4984) --- packages/android/app/build.gradle.kts | 4 ++ .../android/app/src/main/AndroidManifest.xml | 9 ++- .../java/com/formbricks/demo/MainActivity.kt | 61 +++++-------------- .../com/formbricks/demo/ui/theme/Color.kt | 11 ---- .../com/formbricks/demo/ui/theme/Theme.kt | 51 ---------------- .../java/com/formbricks/demo/ui/theme/Type.kt | 33 ---------- .../app/src/main/res/layout/activity_main.xml | 21 +++++++ .../android/formbricksSDK/consumer-rules.pro | 3 +- .../android/formbricksSDK/proguard-rules.pro | 9 +-- .../model/environment/ActionClass.kt | 4 -- .../model/environment/ActionClassReference.kt | 4 -- .../model/environment/EnvironmentData.kt | 4 -- .../environment/EnvironmentResponseData.kt | 4 -- .../model/environment/Project.kt | 4 -- .../formbrickssdk/model/environment/Survey.kt | 5 -- .../network/FormbricksApiService.kt | 5 +- .../webview/FormbricksFragment.kt | 2 + packages/android/gradle/libs.versions.toml | 4 ++ 18 files changed, 59 insertions(+), 179 deletions(-) delete mode 100644 packages/android/app/src/main/java/com/formbricks/demo/ui/theme/Color.kt delete mode 100644 packages/android/app/src/main/java/com/formbricks/demo/ui/theme/Theme.kt delete mode 100644 packages/android/app/src/main/java/com/formbricks/demo/ui/theme/Type.kt create mode 100644 packages/android/app/src/main/res/layout/activity_main.xml diff --git a/packages/android/app/build.gradle.kts b/packages/android/app/build.gradle.kts index 8ea81d2ebb..12be7295a9 100644 --- a/packages/android/app/build.gradle.kts +++ b/packages/android/app/build.gradle.kts @@ -52,6 +52,10 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/packages/android/app/src/main/AndroidManifest.xml b/packages/android/app/src/main/AndroidManifest.xml index e57bab9e9d..f6837d0a88 100644 --- a/packages/android/app/src/main/AndroidManifest.xml +++ b/packages/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools" > + tools:targetApi="31" > + android:theme="@style/Theme.AppCompat.Light.NoActionBar" + android:exported="true" > diff --git a/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt b/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt index baacbbbfd1..0ef212b663 100644 --- a/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt +++ b/packages/android/app/src/main/java/com/formbricks/demo/MainActivity.kt @@ -1,31 +1,21 @@ package com.formbricks.demo import android.os.Bundle -import androidx.activity.compose.setContent +import android.widget.Button import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity -import com.formbricks.demo.ui.theme.DemoTheme +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import com.formbricks.formbrickssdk.Formbricks import com.formbricks.formbrickssdk.helper.FormbricksConfig import java.util.UUID -class MainActivity : FragmentActivity() { +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() - val config = FormbricksConfig.Builder("[API_HOST]","[ENVIRONMENT_ID]") + val config = FormbricksConfig.Builder("[appUrl]","[environmentId]") .setLoggingEnabled(true) .setFragmentManager(supportFragmentManager) Formbricks.setup(this, config.build()) @@ -33,39 +23,16 @@ class MainActivity : FragmentActivity() { Formbricks.logout() Formbricks.setUserId(UUID.randomUUID().toString()) - enableEdgeToEdge() - setContent { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - DemoTheme { - FormbricksDemo() - } - } + setContentView(R.layout.activity_main) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets } - } -} -@Composable -fun FormbricksDemo() { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button(onClick = { + val button = findViewById
+

{t("environments.surveys.summary.you_can_do_a_lot_more_with_links_surveys")} 💡 diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options.ts new file mode 100644 index 0000000000..300c58c271 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options.ts @@ -0,0 +1,36 @@ +import { Options } from "qr-code-styling"; + +export const getQRCodeOptions = (width: number, height: number): Options => ({ + width, + height, + type: "svg", + data: "", + margin: 0, + qrOptions: { + typeNumber: 0, + mode: "Byte", + errorCorrectionLevel: "L", + }, + imageOptions: { + saveAsBlob: true, + hideBackgroundDots: false, + imageSize: 0, + margin: 0, + }, + dotsOptions: { + type: "extra-rounded", + color: "#000000", + roundSize: true, + }, + backgroundOptions: { + color: "#ffffff", + }, + cornersSquareOptions: { + type: "dot", + color: "#000000", + }, + cornersDotOptions: { + type: "dot", + color: "#000000", + }, +}); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code.tsx new file mode 100644 index 0000000000..2a74b21961 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { getQRCodeOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/get-qr-code-options"; +import { useTranslate } from "@tolgee/react"; +import QRCodeStyling from "qr-code-styling"; +import { useEffect, useRef } from "react"; +import { toast } from "react-hot-toast"; + +export const useSurveyQRCode = (surveyUrl: string) => { + const qrCodeRef = useRef(null); + const qrInstance = useRef(null); + const { t } = useTranslate(); + + useEffect(() => { + try { + if (!qrInstance.current) { + qrInstance.current = new QRCodeStyling(getQRCodeOptions(70, 70)); + } + + if (surveyUrl && qrInstance.current) { + qrInstance.current.update({ data: surveyUrl }); + + if (qrCodeRef.current) { + qrCodeRef.current.innerHTML = ""; + qrInstance.current.append(qrCodeRef.current); + } + } + } catch (error) { + toast.error(t("environments.surveys.summary.failed_to_generate_qr_code")); + } + }, [surveyUrl]); + + const downloadQRCode = () => { + try { + const downloadInstance = new QRCodeStyling(getQRCodeOptions(500, 500)); + downloadInstance.update({ data: surveyUrl }); + downloadInstance.download({ name: "survey-qr", extension: "png" }); + } catch (error) { + toast.error(t("environments.surveys.summary.failed_to_generate_qr_code")); + } + }; + + return { qrCodeRef, downloadQRCode }; +}; diff --git a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx index 33528d9f51..70b983eb1b 100644 --- a/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx +++ b/apps/web/modules/analysis/components/ShareSurveyLink/index.tsx @@ -1,10 +1,11 @@ "use client"; +import { useSurveyQRCode } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey-qr-code"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { generateSingleUseIdAction } from "@/modules/survey/list/actions"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; -import { Copy, RefreshCcw, SquareArrowOutUpRight } from "lucide-react"; +import { Copy, QrCode, RefreshCcw, SquareArrowOutUpRight } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -68,6 +69,8 @@ export const ShareSurveyLink = ({ getUrl(); }, [survey, getUrl, language]); + const { downloadQRCode } = useSurveyQRCode(surveyUrl); + return (

@@ -100,6 +103,14 @@ export const ShareSurveyLink = ({ {t("common.copy")} + {survey.singleUse?.enabled && (

If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and @@ -158,7 +155,7 @@ export default function AppPage(): React.JSX.Element {

This button sends a{" "} @@ -166,7 +163,7 @@ export default function AppPage(): React.JSX.Element { {" "} as long as you created it beforehand in the Formbricks App.{" "} @@ -175,6 +172,7 @@ export default function AppPage(): React.JSX.Element {

+ + + + +
+
+ +
+
+

+ This button sets the{" "} + + language + {" "} + to 'de'. +

+
+
+ +
+
+ +
+
+

+ This button sends a{" "} + + Code Action + {" "} + as long as you created it beforehand in the Formbricks App.{" "} + + Here are instructions on how to do it. + +

+
+
+ +
+
+ +
+
+

+ This button logs out the user and syncs the local state with Formbricks. (Only works if a + userId is set) +

+
+
diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx index fa6c476a4c..33822ca1c9 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/components/OnboardingSetupInstructions.tsx @@ -34,10 +34,9 @@ export const OnboardingSetupInstructions = ({ const htmlSnippetForAppSurveys = ` `; @@ -45,9 +44,9 @@ export const OnboardingSetupInstructions = ({ const htmlSnippetForWebsiteSurveys = ` `; @@ -56,10 +55,9 @@ export const OnboardingSetupInstructions = ({ import formbricks from "@formbricks/js"; if (typeof window !== "undefined") { - formbricks.init({ + formbricks.setup({ environmentId: "${environmentId}", - apiHost: "${webAppUrl}", - userId: "testUser", + appUrl: "${webAppUrl}", }); } @@ -75,9 +73,9 @@ export const OnboardingSetupInstructions = ({ import formbricks from "@formbricks/js"; if (typeof window !== "undefined") { - formbricks.init({ + formbricks.setup({ environmentId: "${environmentId}", - apiHost: "${webAppUrl}", + appUrl: "${webAppUrl}", }); } diff --git a/apps/web/app/(app)/components/FormbricksClient.tsx b/apps/web/app/(app)/components/FormbricksClient.tsx index 62b9c35d51..e174e3db6c 100644 --- a/apps/web/app/(app)/components/FormbricksClient.tsx +++ b/apps/web/app/(app)/components/FormbricksClient.tsx @@ -12,12 +12,12 @@ export const FormbricksClient = ({ userId, email }: { userId: string; email: str useEffect(() => { if (formbricksEnabled && userId) { - formbricks.init({ + formbricks.setup({ environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "", - apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "", - userId, + appUrl: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "", }); + formbricks.setUserId(userId); formbricks.setEmail(email); } }, [userId, email]); diff --git a/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts b/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts index f1023d61e3..db59c34ea2 100644 --- a/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts +++ b/apps/web/modules/ee/contacts/api/client/[environmentId]/user/route.ts @@ -5,6 +5,7 @@ import { NextRequest, userAgent } from "next/server"; import { TContactAttributes } from "@formbricks/types/contact-attribute"; import { ResourceNotFoundError } from "@formbricks/types/errors"; import { TJsPersonState, ZJsUserIdentifyInput, ZJsUserUpdateInput } from "@formbricks/types/js"; +import { ZUserEmail } from "@formbricks/types/user"; import { updateUser } from "./lib/update-user"; export const OPTIONS = async (): Promise => { @@ -43,6 +44,17 @@ export const POST = async ( ); } + // validate email if present in attributes + if (parsedInput.data.attributes?.email) { + const emailValidation = ZUserEmail.safeParse(parsedInput.data.attributes.email); + if (!emailValidation.success) { + return responses.badRequestResponse( + "Invalid email", + transformErrorToDetails(emailValidation.error), + true + ); + } + } const { userId, attributes } = parsedInput.data; const isContactsEnabled = await getIsContactsEnabled(); diff --git a/apps/web/modules/ee/insights/components/insights-view.tsx b/apps/web/modules/ee/insights/components/insights-view.tsx index 959d9cace5..e9a77cf5a2 100644 --- a/apps/web/modules/ee/insights/components/insights-view.tsx +++ b/apps/web/modules/ee/insights/components/insights-view.tsx @@ -43,19 +43,8 @@ export const InsightView = ({ const [activeTab, setActiveTab] = useState("all"); const [visibleInsights, setVisibleInsights] = useState(10); - const handleFeedback = (feedback: "positive" | "negative") => { - formbricks.track("AI Insight Feedback", { - hiddenFields: { - feedbackSentiment: feedback, - insightId: currentInsight?.id, - insightTitle: currentInsight?.title, - insightDescription: currentInsight?.description, - insightCategory: currentInsight?.category, - environmentId: currentInsight?.environmentId, - surveyId, - questionId, - }, - }); + const handleFeedback = (_feedback: "positive" | "negative") => { + formbricks.track("AI Insight Feedback"); }; const handleFilterSelect = useCallback( diff --git a/apps/web/modules/ee/insights/experience/components/insight-view.tsx b/apps/web/modules/ee/insights/experience/components/insight-view.tsx index 0202f688ba..7065592b34 100644 --- a/apps/web/modules/ee/insights/experience/components/insight-view.tsx +++ b/apps/web/modules/ee/insights/experience/components/insight-view.tsx @@ -42,17 +42,8 @@ export const InsightView = ({ const [currentInsight, setCurrentInsight] = useState(null); const [activeTab, setActiveTab] = useState("featureRequest"); - const handleFeedback = (feedback: "positive" | "negative") => { - formbricks.track("AI Insight Feedback", { - hiddenFields: { - feedbackSentiment: feedback, - insightId: currentInsight?.id, - insightTitle: currentInsight?.title, - insightDescription: currentInsight?.description, - insightCategory: currentInsight?.category, - environmentId: currentInsight?.environmentId, - }, - }); + const handleFeedback = (_feedback: "positive" | "negative") => { + formbricks.track("AI Insight Feedback"); }; const insightsFilter: TInsightFilterCriteria = useMemo( diff --git a/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx b/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx index 9992cee595..1f25876959 100644 --- a/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx +++ b/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx @@ -40,13 +40,15 @@ export const SetupInstructions = ({ environmentId, webAppUrl }: SetupInstruction yarn add @formbricks/js

{t("environments.project.app-connection.step_2")}

{t("environments.project.app-connection.step_2_description")}

- {`import formbricks from "@formbricks/js"; + + {`import formbricks from "@formbricks/js"; if (typeof window !== "undefined") { - formbricks.init({ + formbricks.setup({ environmentId: "${environmentId}", - apiHost: "${webAppUrl}", + appUrl: "${webAppUrl}", }); -}`} +}`} +
  • environmentId :{" "} @@ -55,21 +57,20 @@ if (typeof window !== "undefined") { })}
  • - apiHost:{" "} + appUrl:{" "} {t("environments.project.app-connection.api_host_description")}
- {t("environments.project.app-connection.if_you_are_planning_to")} + {t("environments.project.app-connection.if_you_are_planning_to")}{" "} {t("environments.project.app-connection.identifying_your_users")} {" "} - {t("environments.project.app-connection.you_also_need_to_pass_a")}{" "} - userId {t("environments.project.app-connection.to_the")}{" "} - init {t("environments.project.app-connection.function")}. + {t("environments.project.app-connection.you_can_set_the_user_id_with")}{" "} + formbricks.setUserId(userId)

{t("environments.project.app-connection.step_3")}

@@ -128,7 +129,7 @@ if (typeof window !== "undefined") {

{` `}

Step 2: Debug mode

diff --git a/apps/web/modules/survey/link/components/link-survey.tsx b/apps/web/modules/survey/link/components/link-survey.tsx index a018222dca..f7f2792c63 100644 --- a/apps/web/modules/survey/link/components/link-survey.tsx +++ b/apps/web/modules/survey/link/components/link-survey.tsx @@ -170,7 +170,7 @@ export const LinkSurvey = ({ PRIVACY_URL={PRIVACY_URL} isBrandingEnabled={project.linkSurveyBranding}> var e = document.getElementsByTagName("script")[0]; e.parentNode.insertBefore(t, e), setTimeout(function () { - formbricks.init({ + formbricks.setup({ environmentId: "ENVIRONMENT_ID", - userId: "RANDOM_USER_ID", - apiHost: "http://localhost:3000", + appUrl: "http://localhost:3000", }); }, 500); })(); diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts index 2a580b9125..de87408481 100644 --- a/apps/web/vite.config.mts +++ b/apps/web/vite.config.mts @@ -40,6 +40,7 @@ export default defineConfig({ "**/openapi.ts", // Exclude openapi configuration files "**/openapi-document.ts", // Exclude openapi document files "modules/**/types/**", // Exclude types + "**/*.tsx", // Exclude tsx files ], }, }, diff --git a/docs/development/standards/practices/documentation.mdx b/docs/development/standards/practices/documentation.mdx index 8f08a90d32..631f613b6b 100644 --- a/docs/development/standards/practices/documentation.mdx +++ b/docs/development/standards/practices/documentation.mdx @@ -21,7 +21,6 @@ We use Mintlify to maintain our documentation. You can find more information abo - Document parameters, return types, and potential side effects - Example: - ```typescript /** Creates a new user and initializes their preferences @@ -31,26 +30,23 @@ Creates a new user and initializes their preferences @throws {ValidationError} If name is invalid */ async function createUser(name: string, options: UserOptions): Promise { -// implementation + // implementation } - ``` - 2. **TypeScript Ignore Comments** - When using `@ts-ignore` or `@ts-expect-error`, always include a comment explaining why - Example: - ```typescript // @ts-expect-error -- Required for dynamic function calls -void window.formbricks.init(...args); +void window.formbricks.setup(...args); ``` - ### API Documentation 1. **API Endpoints** + - All new API endpoints must be documented in the OpenAPI specification - Include request/response schemas, authentication requirements, and examples - Document both Client API and Management API endpoints @@ -63,11 +59,12 @@ void window.formbricks.init(...args); ### Feature Documentation - - All new features must include a feature documentation file - - Document the feature's purpose, usage, and implementation details - - Include code examples and best practices +- All new features must include a feature documentation file +- Document the feature's purpose, usage, and implementation details +- Include code examples and best practices ## Working with Mintlify + We use Mintlify to write our documentation. ### File Structure @@ -84,7 +81,6 @@ icon: "appropriate-icon" --- ``` - 2. **Navigation** - Add new pages to the appropriate section in `docs/mint.json` - Follow the existing navigation structure @@ -104,8 +100,8 @@ Important information goes here ``` - 2. **Media and Assets** + - Store images in the appropriate `/images` subdirectory - Use descriptive alt text for all images - Optimize images for web delivery @@ -130,4 +126,4 @@ mintlify dev - Verify all links and references work - Ensure proper formatting and rendering -These documentation requirements ensure that our codebase remains maintainable, accessible, and well-documented for both current and future developers. \ No newline at end of file +These documentation requirements ensure that our codebase remains maintainable, accessible, and well-documented for both current and future developers. diff --git a/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx b/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx index 1aade23bbd..639a3329dc 100644 --- a/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx +++ b/docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx @@ -28,34 +28,27 @@ How to deliver a specific language depends on the survey type (app or link surve ![Formbricks Home](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languages-from-home.webp) - - Click on the **Edit languages** button, to add a new language to your survey ![Survey Language Settings](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languague-settings.webp) - - Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add language** button to add the language to your project. ![Add Multiple Languages to your Project](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/add-languages.webp) - You can come back to this page anytime to add more languages or remove existing ones. - Now, return to the dashboard to create a new survey or edit an existing one. ![Surveys Home](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/surveys-home.webp) - - In the survey editor, scroll down to the **Multiple Languages** section at the bottom and enable the toggle next to it. ![Enable Multi-language for a survey](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/enable-multi-lang.webp) - Choose a **Default Language** for your survey. - - Changing the default language will reset all the translations you have made - for the survey. - +Changing the default language will reset all the translations you have made for the survey. 1. Now, add the languages from the dropdown that you want to support in your survey. @@ -69,28 +62,24 @@ You can come back to this page anytime to add more languages or remove existing ![Enable Multi-language for a survey](/images/xm-and-surveys/surveys/general-features/multi-language-surveys/translate-as-per-language.webp) - 1. Once you are done, click on the **Publish** button to save the survey. ## App Surveys Configuration -1. When you initialise the Formbricks SDK for your user, you can pass a `language` attribute with the language code. This can be either the ISO identifier or the Alias you set when creating the language. The `language` attribute makes sure that this user only sees surveys with a translation in this specific language available. +1. After you setup the Formbricks SDK for your user, you can call the `setLanguage` function with the language code. This can be either the ISO identifier or the Alias you set when creating the language. The `language` attribute makes sure that this user only sees surveys with a translation in this specific language available. ```js javascript -Formbricks.init({ +Formbricks.setup({ environmentId: "", - apiHost: "", - userId: "", - attributes: { - language: "de", // ISO identifier or Alias set when creating language - }, + appUrl: "", }); + +Formbricks.setLanguage("de"); // ISO identifier or Alias set when creating language ``` - If a user has a language assigned, a survey has multi-language activate and it - is missing a translation in the language of the user, the survey will not be - displayed. + If a user has a language assigned, a survey has multi-language activate and it is missing a translation in + the language of the user, the survey will not be displayed. 1. That's it! Now, users with the language attribute set will see the survey in their preferred language. You can start collecting responses in multiple languages and filter them by language on the summary page. diff --git a/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx b/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx index e4ca81fda8..0b99453f41 100644 --- a/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx +++ b/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx @@ -17,14 +17,12 @@ Integrate the **Formbricks App Survey SDK** into your app using multiple options - [Natively add us to your Next.js project, with support for both App and Pages - project + [Natively add us to your Next.js project, with support for both App and Pages project structure.](https://formbricks.com/docs/app-surveys/framework-guides#next-js) - Learn how to use Formbricks' React Native SDK to integrate your surveys into - React Native applications. + Learn how to use Formbricks' React Native SDK to integrate your surveys into React Native applications. @@ -48,10 +46,9 @@ All you need to do is copy a ` ``` @@ -61,7 +58,7 @@ All you need to do is copy a ` `; @@ -46,7 +46,7 @@ export const OnboardingSetupInstructions = ({ !function(){ var appUrl = "${webAppUrl}"; var environmentId = "${environmentId}"; - var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.setup({environmentId: environmentId, appUrl: appUrl })},500)}(); + var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}(); `; diff --git a/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx b/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx index 1f25876959..7035729423 100644 --- a/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx +++ b/apps/web/modules/projects/settings/(setup)/components/setup-instructions.tsx @@ -129,7 +129,7 @@ if (typeof window !== "undefined") {

{` `}

Step 2: Debug mode

diff --git a/apps/web/playwright/js.spec.ts b/apps/web/playwright/js.spec.ts index d3a05ca059..8c3979a890 100644 --- a/apps/web/playwright/js.spec.ts +++ b/apps/web/playwright/js.spec.ts @@ -8,13 +8,14 @@ const HTML_TEMPLATE = ` var t = document.createElement("script"); (t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/js/formbricks.umd.cjs"); var e = document.getElementsByTagName("script")[0]; - e.parentNode.insertBefore(t, e), - setTimeout(function () { - formbricks.setup({ - environmentId: "ENVIRONMENT_ID", - appUrl: "http://localhost:3000", - }); - }, 500); + t.onload = function(){ + if (window.formbricks) { + window.formbricks.setup({environmentId: "ENVIRONMENT_ID", appUrl: "http://localhost:3000"}); + } else { + console.error("Formbricks library failed to load properly. The formbricks object is not available."); + } + }; + e.parentNode.insertBefore(t, e); })(); From b3f336c959dc16b02bf379bf9b119906601c47a8 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:29:31 +0530 Subject: [PATCH 091/411] fix: invite user bug (#5043) Co-authored-by: pandeymangg --- .../ee/role-management/components/add-member-role.tsx | 6 +++--- .../role-management/components/edit-membership-role.tsx | 5 +++-- .../components/invite-member/individual-invite-tab.tsx | 2 +- packages/logger/package.json | 8 ++++++++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/web/modules/ee/role-management/components/add-member-role.tsx b/apps/web/modules/ee/role-management/components/add-member-role.tsx index 3682070ea3..cfba5eb3a2 100644 --- a/apps/web/modules/ee/role-management/components/add-member-role.tsx +++ b/apps/web/modules/ee/role-management/components/add-member-role.tsx @@ -37,7 +37,7 @@ export function AddMemberRole({ let rolesArray = ["member"]; if (isOwner) { - rolesArray.push("owner", "manager"); + rolesArray.push("manager", "owner"); if (isFormbricksCloud) { rolesArray.push("billing"); } @@ -62,7 +62,7 @@ export function AddMemberRole({
value.trim() !== "" })} + /> +
+ +
+ +
+ {/* Permission rows */} + {Object.keys(selectedPermissions).map((key) => { + const permissionIndex = parseInt(key.split("-")[1]); + const permission = selectedPermissions[key]; + return ( +
+ {/* Project dropdown */} +
+ + + + + + {projectOptions.map((option) => ( + { + updateProjectAndEnvironment(key, option.id); + }}> + {option.name} + + ))} + + +
+ + {/* Environment dropdown */} +
+ + + + + + {getEnvironmentOptionsForProject(permission.projectId).map((env) => ( + { + updatePermission(key, "environmentId", env.id); + }}> + {env.type} + + ))} + + +
+ + {/* Permission level dropdown */} +
+ + + + + + {permissionOptions.map((option) => ( + { + updatePermission(key, "permission", option); + }}> + {option} + + ))} + + +
+ + {/* Delete button */} + +
+ ); + })} + + {/* Add permission button */} + +
+
+ +
+ +
+
+
+ Read + Write + + {Object.keys(selectedOrganizationAccess).map((key) => ( + +
{t(getOrganizationAccessKeyDisplayName(key))}
+
+ + setSelectedOrganizationAccessValue(key, "read", newVal) + } + /> +
+
+ + setSelectedOrganizationAccessValue(key, "write", newVal) + } + /> +
+
+ ))} +
+
+
+ +
+ +

{t("environments.project.api_keys.api_key_security_warning")}

+
+
+
+
+
+ + +
+
+ +
+ + ); +}; diff --git a/apps/web/modules/organization/settings/api-keys/components/api-key-list.test.tsx b/apps/web/modules/organization/settings/api-keys/components/api-key-list.test.tsx new file mode 100644 index 0000000000..05d046f717 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/components/api-key-list.test.tsx @@ -0,0 +1,166 @@ +import "@testing-library/jest-dom/vitest"; +import { render } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { TProject } from "@formbricks/types/project"; +import { getApiKeysWithEnvironmentPermissions } from "../lib/api-key"; +import { ApiKeyList } from "./api-key-list"; + +// Mock the getApiKeysWithEnvironmentPermissions function +vi.mock("../lib/api-key", () => ({ + getApiKeysWithEnvironmentPermissions: vi.fn(), +})); + +// Mock @formbricks/lib/constants +vi.mock("@formbricks/lib/constants", () => ({ + INTERCOM_SECRET_KEY: "test-secret-key", + IS_INTERCOM_CONFIGURED: true, + INTERCOM_APP_ID: "test-app-id", + ENCRYPTION_KEY: "test-encryption-key", + ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key", + GITHUB_ID: "test-github-id", + GITHUB_SECRET: "test-githubID", + GOOGLE_CLIENT_ID: "test-google-client-id", + GOOGLE_CLIENT_SECRET: "test-google-client-secret", + AZUREAD_CLIENT_ID: "test-azuread-client-id", + AZUREAD_CLIENT_SECRET: "test-azure", + AZUREAD_TENANT_ID: "test-azuread-tenant-id", + OIDC_DISPLAY_NAME: "test-oidc-display-name", + OIDC_CLIENT_ID: "test-oidc-client-id", + OIDC_ISSUER: "test-oidc-issuer", + OIDC_CLIENT_SECRET: "test-oidc-client-secret", + OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm", + WEBAPP_URL: "test-webapp-url", +})); + +// Mock @formbricks/lib/env +vi.mock("@formbricks/lib/env", () => ({ + env: { + IS_FORMBRICKS_CLOUD: "0", + }, +})); + +const baseProject = { + id: "project1", + name: "Project 1", + createdAt: new Date(), + updatedAt: new Date(), + organizationId: "org1", + styling: { + allowStyleOverwrite: true, + brandColor: { light: "#000000" }, + }, + recontactDays: 0, + inAppSurveyBranding: false, + linkSurveyBranding: false, + config: { + channel: "link" as const, + industry: "saas" as const, + }, + placement: "bottomLeft" as const, + clickOutsideClose: true, + darkOverlay: false, + languages: [], +}; + +const mockProjects: TProject[] = [ + { + ...baseProject, + environments: [ + { + id: "env1", + type: "production", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + { + id: "env2", + type: "development", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + ], + }, +]; + +const mockApiKeys = [ + { + id: "key1", + hashedKey: "hashed1", + label: "Test Key 1", + createdAt: new Date(), + lastUsedAt: null, + organizationId: "org1", + createdBy: "user1", + }, + { + id: "key2", + hashedKey: "hashed2", + label: "Test Key 2", + createdAt: new Date(), + lastUsedAt: null, + organizationId: "org1", + createdBy: "user1", + }, +]; + +describe("ApiKeyList", () => { + it("renders EditAPIKeys with correct props", async () => { + // Mock the getApiKeysWithEnvironmentPermissions function to return our mock data + (getApiKeysWithEnvironmentPermissions as unknown as ReturnType).mockResolvedValue( + mockApiKeys + ); + + const props = { + organizationId: "org1", + locale: "en-US" as const, + isReadOnly: false, + projects: mockProjects, + }; + + const { container } = render(await ApiKeyList(props)); + + // Verify that EditAPIKeys is rendered with the correct props + expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1"); + expect(container).toBeInTheDocument(); + }); + + it("handles empty api keys", async () => { + // Mock the getApiKeysWithEnvironmentPermissions function to return empty array + (getApiKeysWithEnvironmentPermissions as unknown as ReturnType).mockResolvedValue([]); + + const props = { + organizationId: "org1", + locale: "en-US" as const, + isReadOnly: false, + projects: mockProjects, + }; + + const { container } = render(await ApiKeyList(props)); + + // Verify that EditAPIKeys is rendered even with empty api keys + expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1"); + expect(container).toBeInTheDocument(); + }); + + it("passes isReadOnly prop correctly", async () => { + (getApiKeysWithEnvironmentPermissions as unknown as ReturnType).mockResolvedValue( + mockApiKeys + ); + + const props = { + organizationId: "org1", + locale: "en-US" as const, + isReadOnly: true, + projects: mockProjects, + }; + + const { container } = render(await ApiKeyList(props)); + + expect(getApiKeysWithEnvironmentPermissions).toHaveBeenCalledWith("org1"); + expect(container).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/organization/settings/api-keys/components/api-key-list.tsx b/apps/web/modules/organization/settings/api-keys/components/api-key-list.tsx new file mode 100644 index 0000000000..84525a2fe7 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/components/api-key-list.tsx @@ -0,0 +1,25 @@ +import { getApiKeysWithEnvironmentPermissions } from "@/modules/organization/settings/api-keys/lib/api-key"; +import { TOrganizationProject } from "@/modules/organization/settings/api-keys/types/api-keys"; +import { TUserLocale } from "@formbricks/types/user"; +import { EditAPIKeys } from "./edit-api-keys"; + +interface ApiKeyListProps { + organizationId: string; + locale: TUserLocale; + isReadOnly: boolean; + projects: TOrganizationProject[]; +} + +export const ApiKeyList = async ({ organizationId, locale, isReadOnly, projects }: ApiKeyListProps) => { + const apiKeys = await getApiKeysWithEnvironmentPermissions(organizationId); + + return ( + + ); +}; diff --git a/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.test.tsx b/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.test.tsx new file mode 100644 index 0000000000..c8ba9e5be3 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.test.tsx @@ -0,0 +1,257 @@ +import { ApiKeyPermission } from "@prisma/client"; +import "@testing-library/jest-dom/vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import toast from "react-hot-toast"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { TProject } from "@formbricks/types/project"; +import { createApiKeyAction, deleteApiKeyAction } from "../actions"; +import { TApiKeyWithEnvironmentPermission } from "../types/api-keys"; +import { EditAPIKeys } from "./edit-api-keys"; + +// Mock the actions +vi.mock("../actions", () => ({ + createApiKeyAction: vi.fn(), + deleteApiKeyAction: vi.fn(), +})); + +// Mock react-hot-toast +vi.mock("react-hot-toast", () => ({ + default: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +// Mock the translate hook from @tolgee/react +vi.mock("@tolgee/react", () => ({ + useTranslate: () => ({ + t: (key: string) => key, // simply return the key + }), +})); + +// Base project setup +const baseProject = {}; + +// Example project data +const mockProjects: TProject[] = [ + { + ...baseProject, + id: "project1", + name: "Project 1", + createdAt: new Date(), + updatedAt: new Date(), + organizationId: "org1", + styling: { + allowStyleOverwrite: true, + brandColor: { light: "#000000" }, + }, + config: { + channel: "link" as const, + industry: "saas" as const, + }, + environments: [ + { + id: "env1", + type: "production", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + { + id: "env2", + type: "development", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + ], + } as TProject, +]; + +// Example API keys +const mockApiKeys: TApiKeyWithEnvironmentPermission[] = [ + { + id: "key1", + label: "Test Key 1", + createdAt: new Date(), + organizationAccess: { + accessControl: { + read: true, + write: false, + }, + }, + apiKeyEnvironments: [ + { + environmentId: "env1", + permission: ApiKeyPermission.read, + }, + ], + }, + { + id: "key2", + label: "Test Key 2", + createdAt: new Date(), + organizationAccess: { + accessControl: { + read: true, + write: false, + }, + }, + apiKeyEnvironments: [ + { + environmentId: "env2", + permission: ApiKeyPermission.read, + }, + ], + }, +]; + +describe("EditAPIKeys", () => { + // Reset environment after each test + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + const defaultProps = { + organizationId: "org1", + apiKeys: mockApiKeys, + locale: "en-US" as const, + isReadOnly: false, + projects: mockProjects, + }; + + it("renders the API keys list", () => { + render(); + expect(screen.getByText("common.label")).toBeInTheDocument(); + expect(screen.getByText("Test Key 1")).toBeInTheDocument(); + expect(screen.getByText("Test Key 2")).toBeInTheDocument(); + }); + + it("renders empty state when no API keys", () => { + render(); + expect(screen.getByText("environments.project.api_keys.no_api_keys_yet")).toBeInTheDocument(); + }); + + it("shows add API key button when not readonly", () => { + render(); + expect( + screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" }) + ).toBeInTheDocument(); + }); + + it("hides add API key button when readonly", () => { + render(); + expect( + screen.queryByRole("button", { name: "environments.settings.api_keys.add_api_key" }) + ).not.toBeInTheDocument(); + }); + + it("opens add API key modal when clicking add button", async () => { + render(); + const addButton = screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" }); + await userEvent.click(addButton); + + // Look for the modal title specifically + const modalTitle = screen.getByText("environments.project.api_keys.add_api_key", { + selector: "div.text-xl", + }); + expect(modalTitle).toBeInTheDocument(); + }); + + it("handles API key deletion", async () => { + (deleteApiKeyAction as unknown as ReturnType).mockResolvedValue({ data: true }); + + render(); + const deleteButtons = screen.getAllByRole("button", { name: "" }); // Trash icons + + // Click delete button for first API key + await userEvent.click(deleteButtons[0]); + const confirmDeleteButton = screen.getByRole("button", { name: "common.delete" }); + await userEvent.click(confirmDeleteButton); + + expect(deleteApiKeyAction).toHaveBeenCalledWith({ id: "key1" }); + expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_deleted"); + }); + + it("handles API key creation", async () => { + const newApiKey: TApiKeyWithEnvironmentPermission = { + id: "key3", + label: "New Key", + createdAt: new Date(), + organizationAccess: { + accessControl: { + read: true, + write: false, + }, + }, + apiKeyEnvironments: [ + { + environmentId: "env2", + permission: ApiKeyPermission.read, + }, + ], + }; + + (createApiKeyAction as unknown as ReturnType).mockResolvedValue({ data: newApiKey }); + + render(); + + // Open add modal + const addButton = screen.getByRole("button", { name: "environments.settings.api_keys.add_api_key" }); + await userEvent.click(addButton); + + // Fill in form + const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); + await userEvent.type(labelInput, "New Key"); + + // Optionally toggle the read switch + const readSwitch = screen.getByTestId("organization-access-accessControl-read"); // first is read, second is write + await userEvent.click(readSwitch); // toggle 'read' to true + + // Submit form + const submitButton = screen.getByRole("button", { name: "environments.project.api_keys.add_api_key" }); + await userEvent.click(submitButton); + + expect(createApiKeyAction).toHaveBeenCalledWith({ + organizationId: "org1", + apiKeyData: { + label: "New Key", + environmentPermissions: [{ environmentId: "env1", permission: ApiKeyPermission.read }], + organizationAccess: { + accessControl: { read: true, write: false }, + }, + }, + }); + + expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_created"); + }); + + it("handles copy to clipboard", async () => { + // Mock the clipboard writeText method + const writeText = vi.fn(); + Object.assign(navigator, { + clipboard: { + writeText, + }, + }); + + // Provide an API key that has an actualKey + const apiKeyWithActual = { + ...mockApiKeys[0], + actualKey: "test-api-key-123", + } as TApiKeyWithEnvironmentPermission & { actualKey: string }; + + render(); + + // Find the copy icon button by testid + const copyButton = screen.getByTestId("copy-button"); + await userEvent.click(copyButton); + + expect(writeText).toHaveBeenCalledWith("test-api-key-123"); + expect(toast.success).toHaveBeenCalledWith("environments.project.api_keys.api_key_copied_to_clipboard"); + }); +}); diff --git a/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.tsx b/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.tsx new file mode 100644 index 0000000000..a11ce6e60a --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/components/edit-api-keys.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { ViewPermissionModal } from "@/modules/organization/settings/api-keys/components/view-permission-modal"; +import { + TApiKeyWithEnvironmentPermission, + TOrganizationProject, +} from "@/modules/organization/settings/api-keys/types/api-keys"; +import { Button } from "@/modules/ui/components/button"; +import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; +import { ApiKeyPermission } from "@prisma/client"; +import { useTranslate } from "@tolgee/react"; +import { FilesIcon, TrashIcon } from "lucide-react"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { timeSince } from "@formbricks/lib/time"; +import { TOrganizationAccess } from "@formbricks/types/api-key"; +import { TUserLocale } from "@formbricks/types/user"; +import { createApiKeyAction, deleteApiKeyAction } from "../actions"; +import { AddApiKeyModal } from "./add-api-key-modal"; + +interface EditAPIKeysProps { + organizationId: string; + apiKeys: TApiKeyWithEnvironmentPermission[]; + locale: TUserLocale; + isReadOnly: boolean; + projects: TOrganizationProject[]; +} + +export const EditAPIKeys = ({ organizationId, apiKeys, locale, isReadOnly, projects }: EditAPIKeysProps) => { + const { t } = useTranslate(); + const [isAddAPIKeyModalOpen, setIsAddAPIKeyModalOpen] = useState(false); + const [isDeleteKeyModalOpen, setIsDeleteKeyModalOpen] = useState(false); + const [apiKeysLocal, setApiKeysLocal] = + useState<(TApiKeyWithEnvironmentPermission & { actualKey?: string })[]>(apiKeys); + const [activeKey, setActiveKey] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [viewPermissionsOpen, setViewPermissionsOpen] = useState(false); + + const handleOpenDeleteKeyModal = (e, apiKey) => { + e.preventDefault(); + setActiveKey(apiKey); + setIsDeleteKeyModalOpen(true); + }; + + const handleDeleteKey = async () => { + if (!activeKey) return; + setIsLoading(true); + const deleteApiKeyResponse = await deleteApiKeyAction({ id: activeKey.id }); + if (deleteApiKeyResponse?.data) { + const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || []; + setApiKeysLocal(updatedApiKeys); + toast.success(t("environments.project.api_keys.api_key_deleted")); + setIsDeleteKeyModalOpen(false); + setIsLoading(false); + } else { + toast.error(t("environments.project.api_keys.unable_to_delete_api_key")); + setIsDeleteKeyModalOpen(false); + setIsLoading(false); + } + }; + + const handleAddAPIKey = async (data: { + label: string; + environmentPermissions: Array<{ environmentId: string; permission: ApiKeyPermission }>; + organizationAccess: TOrganizationAccess; + }): Promise => { + setIsLoading(true); + const createApiKeyResponse = await createApiKeyAction({ + organizationId: organizationId, + apiKeyData: { + label: data.label, + environmentPermissions: data.environmentPermissions, + organizationAccess: data.organizationAccess, + }, + }); + + if (createApiKeyResponse?.data) { + const updatedApiKeys = [...apiKeysLocal, createApiKeyResponse.data]; + setApiKeysLocal(updatedApiKeys); + setIsLoading(false); + toast.success(t("environments.project.api_keys.api_key_created")); + } else { + setIsLoading(false); + const errorMessage = getFormattedErrorMessage(createApiKeyResponse); + toast.error(errorMessage); + } + + setIsAddAPIKeyModalOpen(false); + }; + + const ApiKeyDisplay = ({ apiKey }) => { + const copyToClipboard = () => { + navigator.clipboard.writeText(apiKey); + toast.success(t("environments.project.api_keys.api_key_copied_to_clipboard")); + }; + + if (!apiKey) { + return {t("environments.project.api_keys.secret")}; + } + + return ( +
+ {apiKey} +
+ { + e.stopPropagation(); + copyToClipboard(); + }} + data-testid="copy-button" + /> +
+
+ ); + }; + + return ( +
+
+
+
{t("common.label")}
+
+ {t("environments.project.api_keys.api_key")} +
+
{t("common.created_at")}
+
+
+
+ {apiKeysLocal?.length === 0 ? ( +
+ {t("environments.project.api_keys.no_api_keys_yet")} +
+ ) : ( + apiKeysLocal?.map((apiKey) => ( +
{ + setActiveKey(apiKey); + setViewPermissionsOpen(true); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setActiveKey(apiKey); + setViewPermissionsOpen(true); + } + }} + tabIndex={0} + key={apiKey.id}> +
{apiKey.label}
+
+ +
+
+ {timeSince(apiKey.createdAt.toString(), locale)} +
+ {!isReadOnly && ( +
+ +
+ )} +
+ )) + )} +
+
+ + {!isReadOnly && ( +
+ +
+ )} + + {activeKey && ( + + )} + +
+ ); +}; diff --git a/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.test.tsx b/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.test.tsx new file mode 100644 index 0000000000..40ac5a3109 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.test.tsx @@ -0,0 +1,160 @@ +import { ApiKeyPermission } from "@prisma/client"; +import "@testing-library/jest-dom/vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import React from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { TProject } from "@formbricks/types/project"; +import { TApiKeyWithEnvironmentPermission } from "../types/api-keys"; +import { ViewPermissionModal } from "./view-permission-modal"; + +// Mock the translate hook +vi.mock("@tolgee/react", () => ({ + useTranslate: () => ({ + t: (key: string) => key, + }), +})); + +// Base project setup +const baseProject = {}; + +// Example project data +const mockProjects: TProject[] = [ + { + ...baseProject, + id: "project1", + name: "Project 1", + createdAt: new Date(), + updatedAt: new Date(), + organizationId: "org1", + styling: { + allowStyleOverwrite: true, + brandColor: { light: "#000000" }, + }, + config: { + channel: "link" as const, + industry: "saas" as const, + }, + environments: [ + { + id: "env1", + type: "production", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + { + id: "env2", + type: "development", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + ], + } as TProject, +]; + +// Example API key with permissions +const mockApiKey: TApiKeyWithEnvironmentPermission = { + id: "key1", + label: "Test Key 1", + createdAt: new Date(), + organizationAccess: { + accessControl: { + read: true, + write: false, + }, + }, + apiKeyEnvironments: [ + { + environmentId: "env1", + permission: ApiKeyPermission.read, + }, + { + environmentId: "env2", + permission: ApiKeyPermission.write, + }, + ], +}; + +// API key with additional organization access +const mockApiKeyWithOrgAccess = { + ...mockApiKey, + organizationAccess: { + accessControl: { read: true, write: false }, + otherAccess: { read: false, write: true }, + }, +}; + +// API key with no environment permissions +const apiKeyWithoutPermissions = { + ...mockApiKey, + apiKeyEnvironments: [], +}; + +describe("ViewPermissionModal", () => { + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + const defaultProps = { + open: true, + setOpen: vi.fn(), + projects: mockProjects, + apiKey: mockApiKey, + }; + + it("renders the modal with correct title", () => { + render(); + // Check the localized text for the modal's title + expect(screen.getByText("environments.project.api_keys.api_key")).toBeInTheDocument(); + }); + + it("renders all permissions for the API key", () => { + render(); + // The same key has two environment permissions + const projectNames = screen.getAllByText("Project 1"); + expect(projectNames).toHaveLength(2); // once for each permission + expect(screen.getByText("production")).toBeInTheDocument(); + expect(screen.getByText("development")).toBeInTheDocument(); + expect(screen.getByText("read")).toBeInTheDocument(); + expect(screen.getByText("write")).toBeInTheDocument(); + }); + + it("displays correct project and environment names", () => { + render(); + // Check for 'Project 1', 'production', 'development' + const projectNames = screen.getAllByText("Project 1"); + expect(projectNames).toHaveLength(2); + expect(screen.getByText("production")).toBeInTheDocument(); + expect(screen.getByText("development")).toBeInTheDocument(); + }); + + it("displays correct permission levels", () => { + render(); + // Check if permission levels 'read' and 'write' appear + expect(screen.getByText("read")).toBeInTheDocument(); + expect(screen.getByText("write")).toBeInTheDocument(); + }); + + it("handles API key with no permissions", () => { + render(); + // Ensure environment/permission section is empty + expect(screen.queryByText("Project 1")).not.toBeInTheDocument(); + expect(screen.queryByText("production")).not.toBeInTheDocument(); + expect(screen.queryByText("development")).not.toBeInTheDocument(); + }); + + it("displays organizationAccess toggles", () => { + render(); + + expect(screen.getByTestId("organization-access-accessControl-read")).toBeChecked(); + expect(screen.getByTestId("organization-access-accessControl-read")).toBeDisabled(); + expect(screen.getByTestId("organization-access-accessControl-write")).not.toBeChecked(); + expect(screen.getByTestId("organization-access-accessControl-write")).toBeDisabled(); + expect(screen.getByTestId("organization-access-otherAccess-read")).not.toBeChecked(); + expect(screen.getByTestId("organization-access-otherAccess-write")).toBeChecked(); + }); +}); diff --git a/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.tsx b/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.tsx new file mode 100644 index 0000000000..c016d9bbb8 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/components/view-permission-modal.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { getOrganizationAccessKeyDisplayName } from "@/modules/organization/settings/api-keys/lib/utils"; +import { + TApiKeyWithEnvironmentPermission, + TOrganizationProject, +} from "@/modules/organization/settings/api-keys/types/api-keys"; +import { DropdownMenu, DropdownMenuTrigger } from "@/modules/ui/components/dropdown-menu"; +import { Label } from "@/modules/ui/components/label"; +import { Modal } from "@/modules/ui/components/modal"; +import { Switch } from "@/modules/ui/components/switch"; +import { useTranslate } from "@tolgee/react"; +import { Fragment } from "react"; +import { TOrganizationAccess } from "@formbricks/types/api-key"; + +interface ViewPermissionModalProps { + open: boolean; + setOpen: (v: boolean) => void; + apiKey: TApiKeyWithEnvironmentPermission; + projects: TOrganizationProject[]; +} + +export const ViewPermissionModal = ({ open, setOpen, apiKey, projects }: ViewPermissionModalProps) => { + const { t } = useTranslate(); + const organizationAccess = apiKey.organizationAccess as TOrganizationAccess; + + const getProjectName = (environmentId: string) => { + return projects.find((project) => project.environments.find((env) => env.id === environmentId))?.name; + }; + + const getEnvironmentName = (environmentId: string) => { + return projects + .find((project) => project.environments.find((env) => env.id === environmentId)) + ?.environments.find((env) => env.id === environmentId)?.type; + }; + + return ( + +
+
+
+
+
+ {t("environments.project.api_keys.api_key")} +
+
+
+
+
+
+
+
+ +
+ {/* Permission rows */} + {apiKey.apiKeyEnvironments?.map((permission) => { + return ( +
+ {/* Project dropdown */} +
+ + + + + +
+ + {/* Environment dropdown */} +
+ + + + + +
+ + {/* Permission level dropdown */} +
+ + + + + +
+
+ ); + })} +
+
+ +
+ +
+
+
+ Read + Write + + {Object.keys(organizationAccess).map((key) => ( + +
{t(getOrganizationAccessKeyDisplayName(key))}
+
+ +
+
+ +
+
+ ))} +
+
+
+
+
+
+
+
+ ); +}; diff --git a/apps/web/modules/organization/settings/api-keys/lib/api-key.ts b/apps/web/modules/organization/settings/api-keys/lib/api-key.ts new file mode 100644 index 0000000000..45ad8f1b77 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/lib/api-key.ts @@ -0,0 +1,178 @@ +import "server-only"; +import { apiKeyCache } from "@/lib/cache/api-key"; +import { + TApiKeyCreateInput, + TApiKeyWithEnvironmentPermission, + ZApiKeyCreateInput, +} from "@/modules/organization/settings/api-keys/types/api-keys"; +import { ApiKey, ApiKeyPermission, Prisma } from "@prisma/client"; +import { createHash, randomBytes } from "crypto"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { validateInputs } from "@formbricks/lib/utils/validate"; +import { TOrganizationAccess } from "@formbricks/types/api-key"; +import { ZId } from "@formbricks/types/common"; +import { DatabaseError } from "@formbricks/types/errors"; + +export const getApiKeysWithEnvironmentPermissions = reactCache( + async (organizationId: string): Promise => + cache( + async () => { + validateInputs([organizationId, ZId]); + + try { + const apiKeys = await prisma.apiKey.findMany({ + where: { + organizationId, + }, + select: { + id: true, + label: true, + createdAt: true, + organizationAccess: true, + apiKeyEnvironments: { + select: { + environmentId: true, + permission: true, + }, + }, + }, + }); + return apiKeys; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } + }, + [`getApiKeysWithEnvironments-${organizationId}`], + { + tags: [apiKeyCache.tag.byOrganizationId(organizationId)], + } + )() +); + +// Get API key with its permissions from a raw API key +export const getApiKeyWithPermissions = reactCache(async (apiKey: string) => { + const hashedKey = hashApiKey(apiKey); + return cache( + async () => { + // Look up the API key in the new structure + const apiKeyData = await prisma.apiKey.findUnique({ + where: { + hashedKey, + }, + include: { + apiKeyEnvironments: { + include: { + environment: true, + }, + }, + }, + }); + + if (!apiKeyData) return null; + + // Update the last used timestamp + await prisma.apiKey.update({ + where: { + id: apiKeyData.id, + }, + data: { + lastUsedAt: new Date(), + }, + }); + + return apiKeyData; + }, + [`getApiKeyWithPermissions-${apiKey}`], + { + tags: [apiKeyCache.tag.byHashedKey(hashedKey)], + } + )(); +}); + +export const deleteApiKey = async (id: string): Promise => { + validateInputs([id, ZId]); + + try { + const deletedApiKeyData = await prisma.apiKey.delete({ + where: { + id: id, + }, + }); + + apiKeyCache.revalidate({ + id: deletedApiKeyData.id, + hashedKey: deletedApiKeyData.hashedKey, + organizationId: deletedApiKeyData.organizationId, + }); + + return deletedApiKeyData; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } +}; + +const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex"); + +export const createApiKey = async ( + organizationId: string, + userId: string, + apiKeyData: TApiKeyCreateInput & { + environmentPermissions?: Array<{ environmentId: string; permission: ApiKeyPermission }>; + organizationAccess: TOrganizationAccess; + } +): Promise => { + validateInputs([organizationId, ZId], [apiKeyData, ZApiKeyCreateInput]); + try { + const key = randomBytes(16).toString("hex"); + const hashedKey = hashApiKey(key); + + // Extract environmentPermissions from apiKeyData + const { environmentPermissions, organizationAccess, ...apiKeyDataWithoutPermissions } = apiKeyData; + + // Create the API key + const result = await prisma.apiKey.create({ + data: { + ...apiKeyDataWithoutPermissions, + hashedKey, + createdBy: userId, + organization: { connect: { id: organizationId } }, + organizationAccess, + ...(environmentPermissions && environmentPermissions.length > 0 + ? { + apiKeyEnvironments: { + create: environmentPermissions.map((envPerm) => ({ + environmentId: envPerm.environmentId, + permission: envPerm.permission, + })), + }, + } + : {}), + }, + include: { + apiKeyEnvironments: true, + }, + }); + + apiKeyCache.revalidate({ + id: result.id, + hashedKey: result.hashedKey, + organizationId: result.organizationId, + }); + + return { ...result, actualKey: key }; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } +}; diff --git a/apps/web/modules/organization/settings/api-keys/lib/api-keys.test.ts b/apps/web/modules/organization/settings/api-keys/lib/api-keys.test.ts new file mode 100644 index 0000000000..87e3b2dcc5 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/lib/api-keys.test.ts @@ -0,0 +1,194 @@ +import { apiKeyCache } from "@/lib/cache/api-key"; +import { ApiKey, ApiKeyPermission, Prisma } from "@prisma/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { DatabaseError } from "@formbricks/types/errors"; +import { TApiKeyWithEnvironmentPermission } from "../types/api-keys"; +import { createApiKey, deleteApiKey, getApiKeysWithEnvironmentPermissions } from "./api-key"; + +const mockApiKey: ApiKey = { + id: "apikey123", + label: "Test API Key", + hashedKey: "hashed_key_value", + createdAt: new Date(), + createdBy: "user123", + organizationId: "org123", + lastUsedAt: null, + organizationAccess: { + accessControl: { + read: false, + write: false, + }, + }, +}; + +const mockApiKeyWithEnvironments: TApiKeyWithEnvironmentPermission = { + ...mockApiKey, + apiKeyEnvironments: [ + { + environmentId: "env123", + permission: ApiKeyPermission.manage, + }, + ], +}; + +// Mock modules before tests +vi.mock("@formbricks/database", () => ({ + prisma: { + apiKey: { + findMany: vi.fn(), + delete: vi.fn(), + create: vi.fn(), + }, + }, +})); + +vi.mock("@/lib/cache/api-key", () => ({ + apiKeyCache: { + revalidate: vi.fn(), + tag: { + byOrganizationId: vi.fn(), + }, + }, +})); + +vi.mock("crypto", () => ({ + randomBytes: () => ({ + toString: () => "generated_key", + }), + createHash: () => ({ + update: vi.fn().mockReturnThis(), + digest: vi.fn().mockReturnValue("hashed_key_value"), + }), +})); + +describe("API Key Management", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getApiKeysWithEnvironmentPermissions", () => { + it("retrieves API keys successfully", async () => { + vi.mocked(prisma.apiKey.findMany).mockResolvedValueOnce([mockApiKeyWithEnvironments]); + vi.mocked(apiKeyCache.tag.byOrganizationId).mockReturnValue("org-tag"); + + const result = await getApiKeysWithEnvironmentPermissions("clj28r6va000409j3ep7h8xzk"); + + expect(result).toEqual([mockApiKeyWithEnvironments]); + expect(prisma.apiKey.findMany).toHaveBeenCalledWith({ + where: { + organizationId: "clj28r6va000409j3ep7h8xzk", + }, + select: { + apiKeyEnvironments: { + select: { + environmentId: true, + permission: true, + }, + }, + createdAt: true, + id: true, + label: true, + organizationAccess: true, + }, + }); + }); + + it("throws DatabaseError on prisma error", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { + code: "P2002", + clientVersion: "0.0.1", + }); + vi.mocked(prisma.apiKey.findMany).mockRejectedValueOnce(errToThrow); + vi.mocked(apiKeyCache.tag.byOrganizationId).mockReturnValue("org-tag"); + + await expect(getApiKeysWithEnvironmentPermissions("org123")).rejects.toThrow(DatabaseError); + }); + }); + + describe("deleteApiKey", () => { + it("deletes an API key successfully", async () => { + vi.mocked(prisma.apiKey.delete).mockResolvedValueOnce(mockApiKey); + + const result = await deleteApiKey(mockApiKey.id); + + expect(result).toEqual(mockApiKey); + expect(prisma.apiKey.delete).toHaveBeenCalledWith({ + where: { + id: mockApiKey.id, + }, + }); + expect(apiKeyCache.revalidate).toHaveBeenCalled(); + }); + + it("throws DatabaseError on prisma error", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { + code: "P2002", + clientVersion: "0.0.1", + }); + vi.mocked(prisma.apiKey.delete).mockRejectedValueOnce(errToThrow); + + await expect(deleteApiKey(mockApiKey.id)).rejects.toThrow(DatabaseError); + }); + }); + + describe("createApiKey", () => { + const mockApiKeyData = { + label: "Test API Key", + organizationAccess: { + accessControl: { + read: false, + write: false, + }, + }, + }; + + const mockApiKeyWithEnvironments = { + ...mockApiKey, + apiKeyEnvironments: [ + { + id: "env-perm-123", + apiKeyId: "apikey123", + environmentId: "env123", + permission: ApiKeyPermission.manage, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + }; + + it("creates an API key successfully", async () => { + vi.mocked(prisma.apiKey.create).mockResolvedValueOnce(mockApiKey); + + const result = await createApiKey("org123", "user123", mockApiKeyData); + + expect(result).toEqual({ ...mockApiKey, actualKey: "generated_key" }); + expect(prisma.apiKey.create).toHaveBeenCalled(); + expect(apiKeyCache.revalidate).toHaveBeenCalled(); + }); + + it("creates an API key with environment permissions successfully", async () => { + vi.mocked(prisma.apiKey.create).mockResolvedValueOnce(mockApiKeyWithEnvironments); + + const result = await createApiKey("org123", "user123", { + ...mockApiKeyData, + environmentPermissions: [{ environmentId: "env123", permission: ApiKeyPermission.manage }], + }); + + expect(result).toEqual({ ...mockApiKeyWithEnvironments, actualKey: "generated_key" }); + expect(prisma.apiKey.create).toHaveBeenCalled(); + expect(apiKeyCache.revalidate).toHaveBeenCalled(); + }); + + it("throws DatabaseError on prisma error", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { + code: "P2002", + clientVersion: "0.0.1", + }); + + vi.mocked(prisma.apiKey.create).mockRejectedValueOnce(errToThrow); + + await expect(createApiKey("org123", "user123", mockApiKeyData)).rejects.toThrow(DatabaseError); + }); + }); +}); diff --git a/apps/web/modules/organization/settings/api-keys/lib/projects.test.ts b/apps/web/modules/organization/settings/api-keys/lib/projects.test.ts new file mode 100644 index 0000000000..52346cd6d4 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/lib/projects.test.ts @@ -0,0 +1,128 @@ +import { Prisma } from "@prisma/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { projectCache } from "@formbricks/lib/project/cache"; +import { DatabaseError } from "@formbricks/types/errors"; +import { TOrganizationProject } from "../types/api-keys"; +import { getProjectsByOrganizationId } from "./projects"; + +// Mock organization project data +const mockProjects: TOrganizationProject[] = [ + { + id: "project1", + name: "Project 1", + environments: [ + { + id: "env1", + type: "production", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + { + id: "env2", + type: "development", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project1", + appSetupCompleted: true, + }, + ], + }, + { + id: "project2", + name: "Project 2", + environments: [ + { + id: "env3", + type: "production", + createdAt: new Date(), + updatedAt: new Date(), + projectId: "project2", + appSetupCompleted: true, + }, + ], + }, +]; + +vi.mock("@formbricks/database", () => ({ + prisma: { + project: { + findMany: vi.fn(), + }, + }, +})); + +vi.mock("@formbricks/lib/project/cache", () => ({ + projectCache: { + tag: { + byOrganizationId: vi.fn(), + }, + }, +})); + +describe("Projects Management", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getProjectsByOrganizationId", () => { + it("retrieves projects by organization ID successfully", async () => { + vi.mocked(prisma.project.findMany).mockResolvedValueOnce(mockProjects); + vi.mocked(projectCache.tag.byOrganizationId).mockReturnValue("org-tag"); + + const result = await getProjectsByOrganizationId("org123"); + + expect(result).toEqual(mockProjects); + expect(prisma.project.findMany).toHaveBeenCalledWith({ + where: { + organizationId: "org123", + }, + select: { + id: true, + environments: true, + name: true, + }, + }); + }); + + it("returns empty array when no projects exist", async () => { + vi.mocked(prisma.project.findMany).mockResolvedValueOnce([]); + vi.mocked(projectCache.tag.byOrganizationId).mockReturnValue("org-tag"); + + const result = await getProjectsByOrganizationId("org123"); + + expect(result).toEqual([]); + expect(prisma.project.findMany).toHaveBeenCalledWith({ + where: { + organizationId: "org123", + }, + select: { + id: true, + environments: true, + name: true, + }, + }); + }); + + it("throws DatabaseError on prisma error", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { + code: "P2002", + clientVersion: "0.0.1", + }); + vi.mocked(prisma.project.findMany).mockRejectedValueOnce(errToThrow); + vi.mocked(projectCache.tag.byOrganizationId).mockReturnValue("org-tag"); + + await expect(getProjectsByOrganizationId("org123")).rejects.toThrow(DatabaseError); + }); + + it("bubbles up unexpected errors", async () => { + const unexpectedError = new Error("Unexpected error"); + vi.mocked(prisma.project.findMany).mockRejectedValueOnce(unexpectedError); + vi.mocked(projectCache.tag.byOrganizationId).mockReturnValue("org-tag"); + + await expect(getProjectsByOrganizationId("org123")).rejects.toThrow(unexpectedError); + }); + }); +}); diff --git a/apps/web/modules/organization/settings/api-keys/lib/projects.ts b/apps/web/modules/organization/settings/api-keys/lib/projects.ts new file mode 100644 index 0000000000..655bdda3cf --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/lib/projects.ts @@ -0,0 +1,39 @@ +import { TOrganizationProject } from "@/modules/organization/settings/api-keys/types/api-keys"; +import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { projectCache } from "@formbricks/lib/project/cache"; +import { DatabaseError } from "@formbricks/types/errors"; + +export const getProjectsByOrganizationId = reactCache( + async (organizationId: string): Promise => + cache( + async () => { + try { + const projects = await prisma.project.findMany({ + where: { + organizationId, + }, + select: { + id: true, + environments: true, + name: true, + }, + }); + + return projects; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getProjectsByOrganizationId-${organizationId}`], + { + tags: [projectCache.tag.byOrganizationId(organizationId)], + } + )() +); diff --git a/apps/web/modules/organization/settings/api-keys/lib/utils.ts b/apps/web/modules/organization/settings/api-keys/lib/utils.ts new file mode 100644 index 0000000000..489bfa9093 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/lib/utils.ts @@ -0,0 +1,51 @@ +import { TAPIKeyEnvironmentPermission } from "@formbricks/types/auth"; + +// Permission level required for different HTTP methods +const methodPermissionMap = { + GET: "read", // Read operations need at least read permission + POST: "write", // Create operations need at least write permission + PUT: "write", // Update operations need at least write permission + PATCH: "write", // Partial update operations need at least write permission + DELETE: "manage", // Delete operations need manage permission +}; + +// Check if API key has sufficient permission for the requested environment and method +export const hasPermission = ( + permissions: TAPIKeyEnvironmentPermission[], + environmentId: string, + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" +): boolean => { + if (!permissions) return false; + + // Find the environment permission entry for this environment + const environmentPermission = permissions.find((permission) => permission.environmentId === environmentId); + + if (!environmentPermission) return false; + + // Get required permission level for this method + const requiredPermission = methodPermissionMap[method]; + + // Check if the API key has sufficient permission + switch (environmentPermission.permission) { + case "manage": + // Manage permission can do everything + return true; + case "write": + // Write permission can do write and read operations + return requiredPermission === "write" || requiredPermission === "read"; + case "read": + // Read permission can only do read operations + return requiredPermission === "read"; + default: + return false; + } +}; + +export const getOrganizationAccessKeyDisplayName = (key: string) => { + switch (key) { + case "accessControl": + return "environments.project.api_keys.access_control"; + default: + return key; + } +}; diff --git a/apps/web/modules/projects/settings/api-keys/loading.tsx b/apps/web/modules/organization/settings/api-keys/loading.tsx similarity index 80% rename from apps/web/modules/projects/settings/api-keys/loading.tsx rename to apps/web/modules/organization/settings/api-keys/loading.tsx index 1ffa986a7d..0d2bb18169 100644 --- a/apps/web/modules/projects/settings/api-keys/loading.tsx +++ b/apps/web/modules/organization/settings/api-keys/loading.tsx @@ -1,6 +1,6 @@ "use client"; -import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; +import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { useTranslate } from "@tolgee/react"; @@ -19,7 +19,7 @@ const LoadingCard = () => {
{t("common.label")}
- {t("environments.project.api-keys.api_key")} + {t("environments.project.api_keys.api_key")}
{t("common.created_at")}
@@ -38,15 +38,17 @@ const LoadingCard = () => { ); }; -export const APIKeysLoading = () => { +const Loading = ({ isFormbricksCloud }: { isFormbricksCloud: boolean }) => { const { t } = useTranslate(); return ( - - + +
); }; + +export default Loading; diff --git a/apps/web/modules/organization/settings/api-keys/page.tsx b/apps/web/modules/organization/settings/api-keys/page.tsx new file mode 100644 index 0000000000..ddcfae4a89 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/page.tsx @@ -0,0 +1,52 @@ +import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; +import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; +import { getProjectsByOrganizationId } from "@/modules/organization/settings/api-keys/lib/projects"; +import { Alert } from "@/modules/ui/components/alert"; +import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; +import { PageHeader } from "@/modules/ui/components/page-header"; +import { getTranslate } from "@/tolgee/server"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import { ApiKeyList } from "./components/api-key-list"; + +export const APIKeysPage = async (props) => { + const params = await props.params; + const t = await getTranslate(); + const locale = await findMatchingLocale(); + + const { currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId); + + const projects = await getProjectsByOrganizationId(organization.id); + + const isReadOnly = currentUserMembership.role !== "owner" && currentUserMembership.role !== "manager"; + + return ( + + + + + {isReadOnly ? ( + + {t("environments.settings.api_keys.only_organization_owners_and_managers_can_manage_api_keys")} + + ) : ( + + + + )} + + ); +}; diff --git a/apps/web/modules/organization/settings/api-keys/types/api-keys.ts b/apps/web/modules/organization/settings/api-keys/types/api-keys.ts new file mode 100644 index 0000000000..7fa5a986c6 --- /dev/null +++ b/apps/web/modules/organization/settings/api-keys/types/api-keys.ts @@ -0,0 +1,47 @@ +import { ApiKey, ApiKeyPermission } from "@prisma/client"; +import { z } from "zod"; +import { ZApiKey } from "@formbricks/database/zod/api-keys"; +import { ZOrganizationAccess } from "@formbricks/types/api-key"; +import { ZEnvironment } from "@formbricks/types/environment"; + +export const ZApiKeyEnvironmentPermission = z.object({ + environmentId: z.string(), + permission: z.nativeEnum(ApiKeyPermission), +}); + +export const ZApiKeyCreateInput = ZApiKey.required({ + label: true, +}) + .pick({ + label: true, + }) + .extend({ + environmentPermissions: z.array(ZApiKeyEnvironmentPermission).optional(), + organizationAccess: ZOrganizationAccess, + }); + +export type TApiKeyCreateInput = z.infer; + +export interface TApiKey extends ApiKey { + apiKey?: string; +} + +export const OrganizationProject = z.object({ + id: z.string(), + name: z.string(), + environments: z.array(ZEnvironment), +}); + +export type TOrganizationProject = z.infer; + +export const TApiKeyEnvironmentPermission = z.object({ + environmentId: z.string(), + permission: z.nativeEnum(ApiKeyPermission), +}); + +export type TApiKeyEnvironmentPermission = z.infer; + +export interface TApiKeyWithEnvironmentPermission + extends Pick { + apiKeyEnvironments: TApiKeyEnvironmentPermission[]; +} diff --git a/apps/web/modules/projects/settings/api-keys/components/add-api-key-modal.tsx b/apps/web/modules/projects/settings/api-keys/components/add-api-key-modal.tsx deleted file mode 100644 index 3d44191164..0000000000 --- a/apps/web/modules/projects/settings/api-keys/components/add-api-key-modal.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { Button } from "@/modules/ui/components/button"; -import { Input } from "@/modules/ui/components/input"; -import { Label } from "@/modules/ui/components/label"; -import { Modal } from "@/modules/ui/components/modal"; -import { useTranslate } from "@tolgee/react"; -import { AlertTriangleIcon } from "lucide-react"; -import { useForm } from "react-hook-form"; - -interface MemberModalProps { - open: boolean; - setOpen: (v: boolean) => void; - onSubmit: (data: { label: string; environment: string }) => void; -} - -export const AddApiKeyModal = ({ open, setOpen, onSubmit }: MemberModalProps) => { - const { t } = useTranslate(); - const { register, getValues, handleSubmit, reset } = useForm<{ label: string; environment: string }>(); - - const submitAPIKey = async () => { - const data = getValues(); - onSubmit(data); - setOpen(false); - reset(); - }; - - return ( - -
-
-
-
-
- {t("environments.project.api-keys.add_api_key")} -
-
-
-
-
-
-
-
- - value.trim() !== "" })} - /> -
- -
- -

{t("environments.project.api-keys.api_key_security_warning")}

-
-
-
-
-
- - -
-
-
-
-
- ); -}; diff --git a/apps/web/modules/projects/settings/api-keys/components/api-key-list.tsx b/apps/web/modules/projects/settings/api-keys/components/api-key-list.tsx deleted file mode 100644 index ab62375135..0000000000 --- a/apps/web/modules/projects/settings/api-keys/components/api-key-list.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { getApiKeys } from "@/modules/projects/settings/api-keys/lib/api-key"; -import { getTranslate } from "@/tolgee/server"; -import { getEnvironments } from "@formbricks/lib/environment/service"; -import { getProjectByEnvironmentId } from "@formbricks/lib/project/service"; -import { TUserLocale } from "@formbricks/types/user"; -import { EditAPIKeys } from "./edit-api-keys"; - -interface ApiKeyListProps { - environmentId: string; - environmentType: string; - locale: TUserLocale; - isReadOnly: boolean; -} - -export const ApiKeyList = async ({ environmentId, environmentType, locale, isReadOnly }: ApiKeyListProps) => { - const t = await getTranslate(); - const findEnvironmentByType = (environments, targetType) => { - for (const environment of environments) { - if (environment.type === targetType) { - return environment.id; - } - } - return null; - }; - - const project = await getProjectByEnvironmentId(environmentId); - if (!project) { - throw new Error(t("common.project_not_found")); - } - - const environments = await getEnvironments(project.id); - const environmentTypeId = findEnvironmentByType(environments, environmentType); - const apiKeys = await getApiKeys(environmentTypeId); - - return ( - - ); -}; diff --git a/apps/web/modules/projects/settings/api-keys/components/edit-api-keys.tsx b/apps/web/modules/projects/settings/api-keys/components/edit-api-keys.tsx deleted file mode 100644 index 31479d4d0f..0000000000 --- a/apps/web/modules/projects/settings/api-keys/components/edit-api-keys.tsx +++ /dev/null @@ -1,162 +0,0 @@ -"use client"; - -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { TApiKey } from "@/modules/projects/settings/api-keys/types/api-keys"; -import { Button } from "@/modules/ui/components/button"; -import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; -import { useTranslate } from "@tolgee/react"; -import { FilesIcon, TrashIcon } from "lucide-react"; -import { useState } from "react"; -import toast from "react-hot-toast"; -import { timeSince } from "@formbricks/lib/time"; -import { TUserLocale } from "@formbricks/types/user"; -import { createApiKeyAction, deleteApiKeyAction } from "../actions"; -import { AddApiKeyModal } from "./add-api-key-modal"; - -interface EditAPIKeysProps { - environmentTypeId: string; - environmentType: string; - apiKeys: TApiKey[]; - environmentId: string; - locale: TUserLocale; - isReadOnly: boolean; -} - -export const EditAPIKeys = ({ - environmentTypeId, - environmentType, - apiKeys, - environmentId, - locale, - isReadOnly, -}: EditAPIKeysProps) => { - const { t } = useTranslate(); - const [isAddAPIKeyModalOpen, setOpenAddAPIKeyModal] = useState(false); - const [isDeleteKeyModalOpen, setOpenDeleteKeyModal] = useState(false); - const [apiKeysLocal, setApiKeysLocal] = useState(apiKeys); - const [activeKey, setActiveKey] = useState({} as any); - - const handleOpenDeleteKeyModal = (e, apiKey) => { - e.preventDefault(); - setActiveKey(apiKey); - setOpenDeleteKeyModal(true); - }; - - const handleDeleteKey = async () => { - try { - await deleteApiKeyAction({ id: activeKey.id }); - const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || []; - setApiKeysLocal(updatedApiKeys); - toast.success(t("environments.project.api-keys.api_key_deleted")); - } catch (e) { - toast.error(t("environments.project.api-keys.unable_to_delete_api_key")); - } finally { - setOpenDeleteKeyModal(false); - } - }; - - const handleAddAPIKey = async (data) => { - const createApiKeyResponse = await createApiKeyAction({ - environmentId: environmentTypeId, - apiKeyData: { label: data.label }, - }); - if (createApiKeyResponse?.data) { - const updatedApiKeys = [...apiKeysLocal!, createApiKeyResponse.data]; - setApiKeysLocal(updatedApiKeys); - toast.success(t("environments.project.api-keys.api_key_created")); - } else { - const errorMessage = getFormattedErrorMessage(createApiKeyResponse); - toast.error(errorMessage); - } - - setOpenAddAPIKeyModal(false); - }; - - const ApiKeyDisplay = ({ apiKey }) => { - const copyToClipboard = () => { - navigator.clipboard.writeText(apiKey); - toast.success(t("environments.project.api-keys.api_key_copied_to_clipboard")); - }; - - if (!apiKey) { - return {t("environments.project.api-keys.secret")}; - } - - return ( -
- {apiKey} -
- -
-
- ); - }; - - return ( -
-
-
-
{t("common.label")}
-
- {t("environments.project.api-keys.api_key")} -
-
{t("common.created_at")}
-
-
-
- {apiKeysLocal && apiKeysLocal.length === 0 ? ( -
- {t("environments.project.api-keys.no_api_keys_yet")} -
- ) : ( - apiKeysLocal && - apiKeysLocal.map((apiKey) => ( -
-
{apiKey.label}
-
- -
-
- {timeSince(apiKey.createdAt.toString(), locale)} -
- {!isReadOnly && ( -
- -
- )} -
- )) - )} -
-
- - {!isReadOnly && ( -
- -
- )} - - -
- ); -}; diff --git a/apps/web/modules/projects/settings/api-keys/lib/api-key.ts b/apps/web/modules/projects/settings/api-keys/lib/api-key.ts deleted file mode 100644 index d341a94d21..0000000000 --- a/apps/web/modules/projects/settings/api-keys/lib/api-key.ts +++ /dev/null @@ -1,103 +0,0 @@ -import "server-only"; -import { apiKeyCache } from "@/lib/cache/api-key"; -import { TApiKeyCreateInput, ZApiKeyCreateInput } from "@/modules/projects/settings/api-keys/types/api-keys"; -import { TApiKey } from "@/modules/projects/settings/api-keys/types/api-keys"; -import { ApiKey, Prisma } from "@prisma/client"; -import { createHash, randomBytes } from "crypto"; -import { cache as reactCache } from "react"; -import { prisma } from "@formbricks/database"; -import { cache } from "@formbricks/lib/cache"; -import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; -import { validateInputs } from "@formbricks/lib/utils/validate"; -import { ZId, ZOptionalNumber } from "@formbricks/types/common"; -import { DatabaseError } from "@formbricks/types/errors"; - -export const getApiKeys = reactCache( - async (environmentId: string, page?: number): Promise => - cache( - async () => { - validateInputs([environmentId, ZId], [page, ZOptionalNumber]); - - try { - const apiKeys = await prisma.apiKey.findMany({ - where: { - environmentId, - }, - take: page ? ITEMS_PER_PAGE : undefined, - skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined, - }); - - return apiKeys; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - throw error; - } - }, - [`getApiKeys-${environmentId}-${page}`], - { - tags: [apiKeyCache.tag.byEnvironmentId(environmentId)], - } - )() -); - -export const deleteApiKey = async (id: string): Promise => { - validateInputs([id, ZId]); - - try { - const deletedApiKeyData = await prisma.apiKey.delete({ - where: { - id: id, - }, - }); - - apiKeyCache.revalidate({ - id: deletedApiKeyData.id, - hashedKey: deletedApiKeyData.hashedKey, - environmentId: deletedApiKeyData.environmentId, - }); - - return deletedApiKeyData; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } -}; - -const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex"); - -export const createApiKey = async ( - environmentId: string, - apiKeyData: TApiKeyCreateInput -): Promise => { - validateInputs([environmentId, ZId], [apiKeyData, ZApiKeyCreateInput]); - try { - const key = randomBytes(16).toString("hex"); - const hashedKey = hashApiKey(key); - - const result = await prisma.apiKey.create({ - data: { - ...apiKeyData, - hashedKey, - environment: { connect: { id: environmentId } }, - }, - }); - - apiKeyCache.revalidate({ - id: result.id, - hashedKey: result.hashedKey, - environmentId: result.environmentId, - }); - - return { ...result, apiKey: key }; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - throw error; - } -}; diff --git a/apps/web/modules/projects/settings/api-keys/page.tsx b/apps/web/modules/projects/settings/api-keys/page.tsx deleted file mode 100644 index a3bd57ab75..0000000000 --- a/apps/web/modules/projects/settings/api-keys/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; -import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; -import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation"; -import { EnvironmentNotice } from "@/modules/ui/components/environment-notice"; -import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; -import { PageHeader } from "@/modules/ui/components/page-header"; -import { getTranslate } from "@/tolgee/server"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; -import { ApiKeyList } from "./components/api-key-list"; - -export const APIKeysPage = async (props) => { - const params = await props.params; - const t = await getTranslate(); - - // Use the new utility to get all required data with authorization checks - const { environment, isReadOnly } = await getEnvironmentAuth(params.environmentId); - - const locale = await findMatchingLocale(); - - return ( - - - - - - {environment.type === "development" ? ( - - - - ) : ( - - - - )} - - ); -}; diff --git a/apps/web/modules/projects/settings/api-keys/types/api-keys.ts b/apps/web/modules/projects/settings/api-keys/types/api-keys.ts deleted file mode 100644 index eb430093f0..0000000000 --- a/apps/web/modules/projects/settings/api-keys/types/api-keys.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiKey } from "@prisma/client"; -import { z } from "zod"; -import { ZApiKey } from "@formbricks/database/zod/api-keys"; - -export const ZApiKeyCreateInput = ZApiKey.required({ - label: true, -}).pick({ - label: true, -}); - -export type TApiKeyCreateInput = z.infer; - -export interface TApiKey extends ApiKey { - apiKey?: string; -} diff --git a/apps/web/modules/projects/settings/components/project-config-navigation.tsx b/apps/web/modules/projects/settings/components/project-config-navigation.tsx index a173c1b7c2..9ffac70618 100644 --- a/apps/web/modules/projects/settings/components/project-config-navigation.tsx +++ b/apps/web/modules/projects/settings/components/project-config-navigation.tsx @@ -2,7 +2,7 @@ import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation"; import { useTranslate } from "@tolgee/react"; -import { BrushIcon, KeyIcon, LanguagesIcon, ListChecksIcon, TagIcon, UsersIcon } from "lucide-react"; +import { BrushIcon, LanguagesIcon, ListChecksIcon, TagIcon, UsersIcon } from "lucide-react"; import { usePathname } from "next/navigation"; interface ProjectConfigNavigationProps { @@ -48,13 +48,6 @@ export const ProjectConfigNavigation = ({ href: `/environments/${environmentId}/project/tags`, current: pathname?.includes("/tags"), }, - { - id: "api-keys", - label: t("common.api_keys"), - icon: , - href: `/environments/${environmentId}/project/api-keys`, - current: pathname?.includes("/api-keys"), - }, { id: "app-connection", label: t("common.website_and_app_connection"), diff --git a/apps/web/package.json b/apps/web/package.json index 1e10f819cb..6d250ecc50 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -155,6 +155,7 @@ "@types/qrcode": "1.5.5", "@types/testing-library__react": "10.2.0", "@vitest/coverage-v8": "2.1.8", + "resize-observer-polyfill": "1.5.1", "vite": "6.2.3", "vite-tsconfig-paths": "5.1.4", "vitest": "3.0.7", diff --git a/apps/web/playwright/lib/utils.ts b/apps/web/playwright/lib/utils.ts index fe8999b413..66b33c66a6 100644 --- a/apps/web/playwright/lib/utils.ts +++ b/apps/web/playwright/lib/utils.ts @@ -13,11 +13,15 @@ export async function loginAndGetApiKey(page: Page, users: UsersFixture) { throw new Error("Unable to parse environmentId from URL"); })(); - await page.goto(`/environments/${environmentId}/project/api-keys`); + await page.goto(`/environments/${environmentId}/settings/api-keys`); - await page.getByRole("button", { name: "Add Production API Key" }).isVisible(); - await page.getByRole("button", { name: "Add Production API Key" }).click(); + await page.getByRole("button", { name: "Add API Key" }).isVisible(); + await page.getByRole("button", { name: "Add API Key" }).click(); await page.getByPlaceholder("e.g. GitHub, PostHog, Slack").fill("E2E Test API Key"); + await page.getByRole("button", { name: "development" }).click(); + await page.getByRole("menuitem", { name: "production" }).click(); + await page.getByRole("button", { name: "read" }).click(); + await page.getByRole("menuitem", { name: "manage" }).click(); await page.getByRole("button", { name: "Add API Key" }).click(); await page.locator(".copyApiKeyIcon").click(); diff --git a/apps/web/playwright/organization.spec.ts b/apps/web/playwright/organization.spec.ts index 5b839afd38..931022b186 100644 --- a/apps/web/playwright/organization.spec.ts +++ b/apps/web/playwright/organization.spec.ts @@ -24,7 +24,7 @@ test.describe("Invite, accept and remove organization member", async () => { await page.locator('[data-testid="members-loading-card"]:first-child').waitFor({ state: "hidden" }); - await page.getByRole("link", { name: "Teams" }).click(); + await page.getByRole("link", { name: "Access Control" }).click(); // Add member button await expect(page.getByRole("button", { name: "Invite member" })).toBeVisible(); @@ -140,8 +140,8 @@ test.describe("Create, update and delete team", async () => { await page.waitForTimeout(2000); await page.waitForLoadState("networkidle"); - await expect(page.getByText("Teams")).toBeVisible(); - await page.getByText("Teams").click(); + await expect(page.getByText("Access Control")).toBeVisible(); + await page.getByText("Access Control").click(); await page.waitForURL(/\/environments\/[^/]+\/settings\/teams/); await expect(page.getByRole("button", { name: "Create new team" })).toBeVisible(); await page.getByRole("button", { name: "Create new team" }).click(); diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts index 8800bff259..aa6fe1497d 100644 --- a/apps/web/vite.config.mts +++ b/apps/web/vite.config.mts @@ -43,6 +43,10 @@ export default defineConfig({ "app/api/(internal)/insights/lib/**/*.ts", "modules/ee/role-management/*.ts", "modules/organization/settings/teams/actions.ts", + "modules/organization/settings/api-keys/lib/**/*.ts", + "app/api/v1/**/*.ts", + "modules/api/v2/management/auth/*.ts", + "modules/organization/settings/api-keys/components/*.tsx", "modules/survey/hooks/*.tsx", "modules/survey/lib/client-utils.ts", "modules/survey/list/components/survey-card.tsx", diff --git a/package.json b/package.json index 21eacdb4db..cb395a3d51 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,7 @@ }, "lint-staged": { "(apps|packages)/**/*.{js,ts,jsx,tsx}": [ - "prettier --write", - "eslint --fix" + "prettier --write" ], "*.json": [ "prettier --write" diff --git a/packages/database/json-types.ts b/packages/database/json-types.ts index 6b33826c1f..dd02168d1b 100644 --- a/packages/database/json-types.ts +++ b/packages/database/json-types.ts @@ -1,6 +1,7 @@ /* eslint-disable import/no-relative-packages -- required for importing types */ /* eslint-disable @typescript-eslint/no-namespace -- using namespaces is required for prisma-json-types-generator */ import { type TActionClassNoCodeConfig } from "../types/action-classes"; +import type { TOrganizationAccess } from "../types/api-key"; import { type TIntegrationConfig } from "../types/integration"; import { type TOrganizationBilling } from "../types/organizations"; import { type TProjectConfig, type TProjectStyling } from "../types/project"; @@ -45,5 +46,6 @@ declare global { export type Locale = TUserLocale; export type SurveyFollowUpTrigger = TSurveyFollowUpTrigger; export type SurveyFollowUpAction = TSurveyFollowUpAction; + export type OrganizationAccess = TOrganizationAccess; } } diff --git a/packages/database/migration/20250326083401_add_api_keys_to_organization/migration.sql b/packages/database/migration/20250326083401_add_api_keys_to_organization/migration.sql new file mode 100644 index 0000000000..c342fa820c --- /dev/null +++ b/packages/database/migration/20250326083401_add_api_keys_to_organization/migration.sql @@ -0,0 +1,48 @@ +-- CreateEnum +CREATE TYPE "ApiKeyPermission" AS ENUM ('read', 'write', 'manage'); + +-- CreateTable +CREATE TABLE "ApiKeyNew" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT, + "lastUsedAt" TIMESTAMP(3), + "label" TEXT NOT NULL, + "hashedKey" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "ApiKeyNew_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ApiKeyEnvironment" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "apiKeyId" TEXT NOT NULL, + "environmentId" TEXT NOT NULL, + "permission" "ApiKeyPermission" NOT NULL, + + CONSTRAINT "ApiKeyEnvironment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ApiKeyNew_hashedKey_key" ON "ApiKeyNew"("hashedKey"); + +-- CreateIndex +CREATE INDEX "ApiKeyNew_organizationId_idx" ON "ApiKeyNew"("organizationId"); + +-- CreateIndex +CREATE INDEX "ApiKeyEnvironment_environmentId_idx" ON "ApiKeyEnvironment"("environmentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ApiKeyEnvironment_apiKeyId_environmentId_key" ON "ApiKeyEnvironment"("apiKeyId", "environmentId"); + +-- AddForeignKey +ALTER TABLE "ApiKeyNew" ADD CONSTRAINT "ApiKeyNew_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ApiKeyEnvironment" ADD CONSTRAINT "ApiKeyEnvironment_apiKeyId_fkey" FOREIGN KEY ("apiKeyId") REFERENCES "ApiKeyNew"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ApiKeyEnvironment" ADD CONSTRAINT "ApiKeyEnvironment_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/migration/20250326111101_move_api_keys_to_api_keys_new/migration.ts b/packages/database/migration/20250326111101_move_api_keys_to_api_keys_new/migration.ts new file mode 100644 index 0000000000..c8005b23cc --- /dev/null +++ b/packages/database/migration/20250326111101_move_api_keys_to_api_keys_new/migration.ts @@ -0,0 +1,83 @@ +import type { MigrationScript } from "../../src/scripts/migration-runner"; + +export const moveApiKeysToApiKeysNew: MigrationScript = { + type: "data", + id: "mvwdryxrxaf8rhr97g2zlv3m", + name: "20250326111101_move_api_keys_to_api_keys_new", + run: async ({ tx }) => { + // Step 1: Get all existing API keys with related data + const apiKeys = await tx.$queryRaw` + SELECT + ak.*, + e.id as "environmentId", + p.id as "projectId", + o.id as "organizationId" + FROM "ApiKey" ak + JOIN "Environment" e ON ak."environmentId" = e.id + JOIN "Project" p ON e."projectId" = p.id + JOIN "Organization" o ON p."organizationId" = o.id + `; + // @ts-expect-error + console.log(`Found ${apiKeys.length} API keys to migrate.`); + let migratedCount = 0; + // Step 2: Migrate each API key to the new format + // @ts-expect-error + for (const apiKey of apiKeys) { + const organizationId = apiKey.organizationId; + + try { + // Check if the API key already exists in the new table + const existingKey = await tx.$queryRaw` + SELECT id FROM "ApiKeyNew" WHERE id = ${apiKey.id} + `; + + if (Array.isArray(existingKey) && existingKey.length > 0) { + continue; + } + + // Check if the API key environment relation already exists + const existingEnv = await tx.$queryRaw` + SELECT id FROM "ApiKeyEnvironment" + WHERE "apiKeyId" = ${apiKey.id} AND "environmentId" = ${apiKey.environmentId} + `; + + if (Array.isArray(existingEnv) && existingEnv.length > 0) { + continue; + } + + // Step 3: Create new API key in the ApiKeyNew table and its environment relation + await tx.$executeRaw` + INSERT INTO "ApiKeyNew" ( + "id", + "createdAt", + "lastUsedAt", + "label", + "hashedKey", + "organizationId" + ) VALUES ( + ${apiKey.id}, + ${apiKey.createdAt}, + ${apiKey.lastUsedAt}, + ${apiKey.label}, + ${apiKey.hashedKey}, + ${organizationId} + ) + `; + + // Create the API key environment relation using Prisma + await tx.apiKeyEnvironment.create({ + data: { + apiKeyId: apiKey.id, + environmentId: apiKey.environmentId, + permission: "manage", + }, + }); + migratedCount++; + } catch (error) { + console.error(`Error migrating API key ${apiKey.id}:`, error); + } + } + + console.log(`API key migration completed. Migrated ${migratedCount} API keys.`); + }, +}; diff --git a/packages/database/migration/20250327043931_api_key_new_to_api_key/migration.sql b/packages/database/migration/20250327043931_api_key_new_to_api_key/migration.sql new file mode 100644 index 0000000000..74f035d2e2 --- /dev/null +++ b/packages/database/migration/20250327043931_api_key_new_to_api_key/migration.sql @@ -0,0 +1,30 @@ +BEGIN; + -- Lock both tables to prevent any modifications during migration + LOCK TABLE "ApiKey" IN ACCESS EXCLUSIVE MODE; + LOCK TABLE "ApiKeyNew" IN ACCESS EXCLUSIVE MODE; + + -- Verify all data is migrated before proceeding + DO $$ + BEGIN + IF (SELECT COUNT(*) FROM "ApiKey") != (SELECT COUNT(*) FROM "ApiKeyNew") THEN + RAISE EXCEPTION 'Data migration incomplete. Counts do not match.'; + END IF; + END $$; + + -- Drop the old ApiKey table first + DROP TABLE IF EXISTS "ApiKey"; + + -- Rename ApiKeyNew to ApiKey + ALTER TABLE "ApiKeyNew" RENAME TO "ApiKey"; + ALTER TABLE "ApiKey" RENAME CONSTRAINT "ApiKeyNew_pkey" TO "ApiKey_pkey"; + ALTER INDEX "ApiKeyNew_hashedKey_key" RENAME TO "ApiKey_hashedKey_key"; + ALTER INDEX "ApiKeyNew_organizationId_idx" RENAME TO "ApiKey_organizationId_idx"; + + -- Update the constraints to maintain foreign key relationships + ALTER TABLE "ApiKeyEnvironment" DROP CONSTRAINT "ApiKeyEnvironment_apiKeyId_fkey"; + ALTER TABLE "ApiKeyEnvironment" ADD CONSTRAINT "ApiKeyEnvironment_apiKeyId_fkey" FOREIGN KEY ("apiKeyId") REFERENCES "ApiKey"("id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- Rename the foreign key constraint + ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKeyNew_organizationId_fkey"; + ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; +COMMIT; \ No newline at end of file diff --git a/packages/database/migration/20250402084646_add_organization_access_to_api_key/migration.sql b/packages/database/migration/20250402084646_add_organization_access_to_api_key/migration.sql new file mode 100644 index 0000000000..4fbb382986 --- /dev/null +++ b/packages/database/migration/20250402084646_add_organization_access_to_api_key/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ApiKey" ADD COLUMN "organizationAccess" JSONB NOT NULL DEFAULT '{}'; diff --git a/packages/database/migration/20250402084801_set_default_organization_access_to_all_existing_api_keys/migration.ts b/packages/database/migration/20250402084801_set_default_organization_access_to_all_existing_api_keys/migration.ts new file mode 100644 index 0000000000..1e50ccfbc1 --- /dev/null +++ b/packages/database/migration/20250402084801_set_default_organization_access_to_all_existing_api_keys/migration.ts @@ -0,0 +1,18 @@ +import type { MigrationScript } from "../../src/scripts/migration-runner"; + +export const setDefaultOrganizationAccessToAllExistingApiKeys: MigrationScript = { + type: "data", + id: "jd54tyjvat97yn9rgkgsneaq", + name: "20250402084801_set_default_organization_access_to_all_existing_api_keys", + run: async ({ tx }) => { + try { + await tx.$queryRaw` + UPDATE "ApiKey" + SET "organizationAccess" = '{"accessControl":{"read":false,"write":false}}' + WHERE "organizationAccess" IS NULL OR "organizationAccess" = '{}' + `; + } catch (error) { + console.error("Error adding organization access to API keys", error); + } + }, +}; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index d0992cb188..b7bf760a77 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -555,13 +555,13 @@ model Environment { contacts Contact[] actionClasses ActionClass[] attributeKeys ContactAttributeKey[] - apiKeys ApiKey[] webhooks Webhook[] tags Tag[] segments Segment[] integration Integration[] documents Document[] insights Insight[] + ApiKeyEnvironment ApiKeyEnvironment[] @@index([projectId]) } @@ -639,6 +639,7 @@ model Organization { invites Invite[] isAIEnabled Boolean @default(false) teams Team[] + apiKeys ApiKey[] } enum OrganizationRole { @@ -711,23 +712,58 @@ model Invite { @@index([organizationId]) } -/// Represents API authentication keys. -/// Used for authenticating API requests to Formbricks. +/// Represents enhanced API authentication keys with organization-level ownership. +/// Used for authenticating API requests to Formbricks with more granular permissions. /// /// @property id - Unique identifier for the API key /// @property label - Optional descriptive name for the key /// @property hashedKey - Securely stored API key -/// @property environment - The environment this key belongs to +/// @property organization - The organization this key belongs to +/// @property createdBy - User ID who created this key /// @property lastUsedAt - Timestamp of last usage +/// @property apiKeyEnvironments - Environments this key has access to model ApiKey { - id String @id @unique @default(cuid()) - createdAt DateTime @default(now()) - lastUsedAt DateTime? - label String? - hashedKey String @unique() - environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) - environmentId String + id String @id @default(cuid()) + createdAt DateTime @default(now()) + createdBy String? + lastUsedAt DateTime? + label String + hashedKey String @unique + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + apiKeyEnvironments ApiKeyEnvironment[] + /// [OrganizationAccess] + organizationAccess Json @default("{}") + @@index([organizationId]) +} + +/// Defines permission levels for API keys. +/// Controls what operations an API key can perform. +enum ApiKeyPermission { + read + write + manage +} + +/// Links API keys to environments with specific permissions. +/// Enables granular access control for API keys across environments. +/// +/// @property id - Unique identifier for the environment access entry +/// @property apiKey - The associated API key +/// @property environment - The environment being accessed +/// @property permission - Level of access granted +model ApiKeyEnvironment { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + apiKeyId String + apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade) + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + permission ApiKeyPermission + + @@unique([apiKeyId, environmentId]) @@index([environmentId]) } diff --git a/packages/database/src/scripts/migration-runner.ts b/packages/database/src/scripts/migration-runner.ts index 2f43c4b986..458798af65 100644 --- a/packages/database/src/scripts/migration-runner.ts +++ b/packages/database/src/scripts/migration-runner.ts @@ -1,8 +1,8 @@ +import { type Prisma, PrismaClient } from "@prisma/client"; import { exec } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; -import { type Prisma, PrismaClient } from "@prisma/client"; import { logger } from "@formbricks/logger"; const execAsync = promisify(exec); @@ -44,6 +44,7 @@ const runMigrations = async (migrations: MigrationScript[]): Promise => { const runSingleMigration = async (migration: MigrationScript, index: number): Promise => { if (migration.type === "data") { + let hasLock = false; logger.info(`Running data migration: ${migration.name}`); try { @@ -76,6 +77,7 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr } else { // create a new data migration entry with pending status await prisma.$executeRaw`INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'pending')`; + hasLock = true; } if (migration.run) { @@ -100,12 +102,15 @@ const runSingleMigration = async (migration: MigrationScript, index: number): Pr } catch (error) { // Record migration failure logger.error(error, `Data migration ${migration.name} failed`); - // Mark migration as failed - await prisma.$queryRaw` - INSERT INTO "DataMigration" (id, name, status) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- we need to check if the migration has a lock + if (hasLock) { + // Mark migration as failed + await prisma.$queryRaw` + INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'failed') - ON CONFLICT (id) DO UPDATE SET status = 'failed'; - `; + ON CONFLICT (id) DO UPDATE SET status = 'failed'; + `; + } throw error; } diff --git a/packages/database/zod/api-keys.ts b/packages/database/zod/api-keys.ts index f63134973c..db15dedde0 100644 --- a/packages/database/zod/api-keys.ts +++ b/packages/database/zod/api-keys.ts @@ -1,11 +1,39 @@ -import { type ApiKey } from "@prisma/client"; +import { type ApiKey, type ApiKeyEnvironment, ApiKeyPermission } from "@prisma/client"; import { z } from "zod"; +import { ZOrganizationAccess } from "../../types/api-key"; + +export const ZApiKeyPermission = z.nativeEnum(ApiKeyPermission); + +export const ZApiKeyEnvironment = z.object({ + id: z.string().cuid2(), + createdAt: z.date(), + updatedAt: z.date(), + apiKeyId: z.string().cuid2(), + environmentId: z.string().cuid2(), + permission: ZApiKeyPermission, +}) satisfies z.ZodType; export const ZApiKey = z.object({ id: z.string().cuid2(), createdAt: z.date(), + createdBy: z.string(), lastUsedAt: z.date().nullable(), - label: z.string().nullable(), + label: z.string(), hashedKey: z.string(), - environmentId: z.string().cuid2(), + organizationId: z.string().cuid2(), + organizationAccess: ZOrganizationAccess, }) satisfies z.ZodType; + +export const ZApiKeyCreateInput = z.object({ + label: z.string(), + organizationId: z.string().cuid2(), + environmentIds: z.array(z.string().cuid2()), + permissions: z.record(z.string().cuid2(), ZApiKeyPermission), + createdBy: z.string(), +}); + +export const ZApiKeyEnvironmentCreateInput = z.object({ + apiKeyId: z.string().cuid2(), + environmentId: z.string().cuid2(), + permission: ZApiKeyPermission, +}); diff --git a/packages/js-core/src/lib/environment/state.ts b/packages/js-core/src/lib/environment/state.ts index d29c54d146..01aa0fbfb3 100644 --- a/packages/js-core/src/lib/environment/state.ts +++ b/packages/js-core/src/lib/environment/state.ts @@ -1,10 +1,10 @@ /* eslint-disable no-console -- logging required for error logging */ -import { FormbricksAPI } from "@formbricks/api"; import { Config } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { filterSurveys, getIsDebug } from "@/lib/common/utils"; import type { TConfigInput, TEnvironmentState } from "@/types/config"; import { type ApiErrorResponse, type Result, err, ok } from "@/types/error"; +import { FormbricksAPI } from "@formbricks/api"; let environmentStateSyncIntervalId: number | null = null; diff --git a/packages/js-core/src/lib/environment/tests/state.test.ts b/packages/js-core/src/lib/environment/tests/state.test.ts index 82274a62f7..628c62850f 100644 --- a/packages/js-core/src/lib/environment/tests/state.test.ts +++ b/packages/js-core/src/lib/environment/tests/state.test.ts @@ -1,6 +1,4 @@ // state.test.ts -import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { FormbricksAPI } from "@formbricks/api"; import { Config } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { filterSurveys } from "@/lib/common/utils"; @@ -10,6 +8,8 @@ import { fetchEnvironmentState, } from "@/lib/environment/state"; import type { TEnvironmentState } from "@/types/config"; +import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { FormbricksAPI } from "@formbricks/api"; // Mock the FormbricksAPI so we can control environment.getState vi.mock("@formbricks/api", () => ({ diff --git a/packages/js-core/src/lib/survey/tests/action.test.ts b/packages/js-core/src/lib/survey/tests/action.test.ts index 59f6bb2c60..f2c7fcb203 100644 --- a/packages/js-core/src/lib/survey/tests/action.test.ts +++ b/packages/js-core/src/lib/survey/tests/action.test.ts @@ -1,9 +1,9 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; import { Config } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { trackAction, trackCodeAction, trackNoCodeAction } from "@/lib/survey/action"; import { SurveyStore } from "@/lib/survey/store"; import { triggerSurvey } from "@/lib/survey/widget"; +import { beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("@/lib/common/config", () => ({ Config: { diff --git a/packages/js-core/src/lib/survey/tests/widget.test.ts b/packages/js-core/src/lib/survey/tests/widget.test.ts index c7095b1188..ed070c4eb7 100644 --- a/packages/js-core/src/lib/survey/tests/widget.test.ts +++ b/packages/js-core/src/lib/survey/tests/widget.test.ts @@ -1,10 +1,10 @@ -import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { mockSurvey } from "@/lib/survey/tests/__mocks__/widget.mock"; import { Config } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { filterSurveys, getLanguageCode, shouldDisplayBasedOnPercentage } from "@/lib/common/utils"; import * as widget from "@/lib/survey/widget"; import { type TEnvironmentStateSurvey } from "@/types/config"; +import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("@/lib/common/config", () => ({ Config: { diff --git a/packages/js-core/src/lib/user/tests/update.test.ts b/packages/js-core/src/lib/user/tests/update.test.ts index 565492d025..91ba546b78 100644 --- a/packages/js-core/src/lib/user/tests/update.test.ts +++ b/packages/js-core/src/lib/user/tests/update.test.ts @@ -1,5 +1,3 @@ -import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; -import { FormbricksAPI } from "@formbricks/api"; import { mockAppUrl, mockAttributes, @@ -10,6 +8,8 @@ import { Config } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { sendUpdates, sendUpdatesToBackend } from "@/lib/user/update"; import { type TUpdates } from "@/types/config"; +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; +import { FormbricksAPI } from "@formbricks/api"; vi.mock("@/lib/common/config", () => ({ Config: { diff --git a/packages/js-core/src/lib/user/update.ts b/packages/js-core/src/lib/user/update.ts index ace4287a99..f3f7031ee7 100644 --- a/packages/js-core/src/lib/user/update.ts +++ b/packages/js-core/src/lib/user/update.ts @@ -1,10 +1,10 @@ /* eslint-disable no-console -- required for logging errors */ -import { FormbricksAPI } from "@formbricks/api"; import { Config } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { filterSurveys, getIsDebug } from "@/lib/common/utils"; import { type TUpdates, type TUserState } from "@/types/config"; import { type ApiErrorResponse, type Result, type ResultError, err, ok, okVoid } from "@/types/error"; +import { FormbricksAPI } from "@formbricks/api"; export const sendUpdatesToBackend = async ({ appUrl, diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index b89826998a..f4e62b6669 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -372,7 +372,7 @@ "team": "Team", "team_access": "Teamzugriff", "team_name": "Teamname", - "teams": "Teams", + "teams": "Zugriffskontrolle", "teams_not_found": "Teams nicht gefunden", "text": "Text", "time": "Zeit", @@ -792,6 +792,29 @@ "secret": "Geheimnis", "unable_to_delete_api_key": "API-Schlüssel kann nicht gelöscht werden" }, + "api_keys": { + "access_control": "Zugriffskontrolle", + "add_api_key": "API-Schlüssel hinzufügen", + "add_env_api_key": "{environmentType} API-Schlüssel hinzufügen", + "api_key": "API-Schlüssel", + "api_key_copied_to_clipboard": "API-Schlüssel in die Zwischenablage kopiert", + "api_key_created": "API-Schlüssel erstellt", + "api_key_deleted": "API-Schlüssel gelöscht", + "api_key_label": "API-Schlüssel Label", + "api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.", + "dev_api_keys": "API-Schlüssel (Dev)", + "dev_api_keys_description": "API-Schlüssel für deine Entwicklungsumgebung hinzufügen und entfernen.", + "duplicate_access": "Doppelter Projektzugriff nicht erlaubt", + "duplicate_permissions": "Doppelte Berechtigungen sind nicht erlaubt", + "no_api_keys_yet": "Du hast noch keine API-Schlüssel", + "organization_access": "Organisationszugang", + "permissions": "Berechtigungen", + "prod_api_keys": "API-Schlüssel (Prod)", + "prod_api_keys_description": "API-Schlüssel für deine Produktionsumgebung hinzufügen und entfernen.", + "project_access": "Projektzugriff", + "secret": "Geheimnis", + "unable_to_delete_api_key": "API-Schlüssel kann nicht gelöscht werden" + }, "app-connection": { "api_host_description": "Dies ist die URL deines Formbricks Backends.", "app_connection": "App-Verbindung", @@ -988,6 +1011,12 @@ "with_the_formbricks_sdk": "mit dem Formbricks SDK" }, "settings": { + "api_keys": { + "add_api_key": "API-Schlüssel hinzufügen", + "add_permission": "Berechtigung hinzufügen", + "api_keys_description": "Verwalte API-Schlüssel, um auf die Formbricks-Management-APIs zuzugreifen", + "only_organization_owners_and_managers_can_manage_api_keys": "Nur Organisationsinhaber und -manager können API-Schlüssel verwalten" + }, "billing": { "10000_monthly_responses": "10,000 monatliche Antworten", "1500_monthly_responses": "1,500 monatliche Antworten", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 0ebab246cb..0cc257ff99 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -372,7 +372,7 @@ "team": "Team", "team_access": "Team Access", "team_name": "Team name", - "teams": "Teams", + "teams": "Access Control", "teams_not_found": "Teams not found", "text": "Text", "time": "Time", @@ -792,6 +792,29 @@ "secret": "Secret", "unable_to_delete_api_key": "Unable to delete API Key" }, + "api_keys": { + "access_control": "Access Control", + "add_api_key": "Add API Key", + "add_env_api_key": "Add {environmentType} API Key", + "api_key": "API Key", + "api_key_copied_to_clipboard": "API key copied to clipboard", + "api_key_created": "API key created", + "api_key_deleted": "API Key deleted", + "api_key_label": "API Key Label", + "api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.", + "dev_api_keys": "Development Env Keys", + "dev_api_keys_description": "Add and remove API keys for your Development environment.", + "duplicate_access": "Duplicate project access not allowed", + "duplicate_permissions": "Duplicate permissions not allowed", + "no_api_keys_yet": "You don't have any API keys yet", + "organization_access": "Organization Access", + "permissions": "Permissions", + "prod_api_keys": "Production Env Keys", + "prod_api_keys_description": "Add and remove API keys for your Production environment.", + "project_access": "Project Access", + "secret": "Secret", + "unable_to_delete_api_key": "Unable to delete API Key" + }, "app-connection": { "api_host_description": "This is the URL of your Formbricks backend.", "app_connection": "App Connection", @@ -988,6 +1011,12 @@ "with_the_formbricks_sdk": "with the Formbricks SDK" }, "settings": { + "api_keys": { + "add_api_key": "Add API key", + "add_permission": "Add permission", + "api_keys_description": "Manage API keys to access Formbricks management APIs", + "only_organization_owners_and_managers_can_manage_api_keys": "Only organization owners and managers can manage API keys" + }, "billing": { "10000_monthly_responses": "10000 Monthly Responses", "1500_monthly_responses": "1500 Monthly Responses", diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index 88750c2443..68578df2a2 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -372,7 +372,7 @@ "team": "Équipe", "team_access": "Accès Équipe", "team_name": "Nom de l'équipe", - "teams": "Équipes", + "teams": "Contrôle d'accès", "teams_not_found": "Équipes non trouvées", "text": "Texte", "time": "Temps", @@ -792,6 +792,29 @@ "secret": "Secret", "unable_to_delete_api_key": "Impossible de supprimer la clé API" }, + "api_keys": { + "access_control": "Contrôle d'accès", + "add_api_key": "Ajouter une clé API", + "add_env_api_key": "Ajouter la clé API {environmentType}", + "api_key": "Clé API", + "api_key_copied_to_clipboard": "Clé API copiée dans le presse-papiers", + "api_key_created": "Clé API créée", + "api_key_deleted": "Clé API supprimée", + "api_key_label": "Étiquette de clé API", + "api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.", + "dev_api_keys": "Clés de l'environnement de développement", + "dev_api_keys_description": "Ajoutez et supprimez des clés API pour votre environnement de développement.", + "duplicate_access": "L'accès en double au projet n'est pas autorisé", + "duplicate_permissions": "Les autorisations en double ne sont pas autorisées", + "no_api_keys_yet": "Vous n'avez pas encore de clés API.", + "organization_access": "Accès à l'organisation", + "permissions": "Permissions", + "prod_api_keys": "Clés de l'environnement de production", + "prod_api_keys_description": "Ajoutez et supprimez des clés API pour votre environnement de production.", + "project_access": "Accès au projet", + "secret": "Secret", + "unable_to_delete_api_key": "Impossible de supprimer la clé API" + }, "app-connection": { "api_host_description": "Ceci est l'URL de votre backend Formbricks.", "app_connection": "Connexion d'application", @@ -988,6 +1011,12 @@ "with_the_formbricks_sdk": "avec le SDK Formbricks" }, "settings": { + "api_keys": { + "add_api_key": "Ajouter une clé API", + "add_permission": "Ajouter une permission", + "api_keys_description": "Gérer les clés API pour accéder aux API de gestion de Formbricks", + "only_organization_owners_and_managers_can_manage_api_keys": "Seuls les propriétaires et les gestionnaires de l'organisation peuvent gérer les clés API" + }, "billing": { "10000_monthly_responses": "10000 Réponses Mensuelles", "1500_monthly_responses": "1500 Réponses Mensuelles", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index e9aa823ab2..80f02023e0 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -372,7 +372,7 @@ "team": "Time", "team_access": "Acesso da equipe", "team_name": "Nome da equipe", - "teams": "Times", + "teams": "Controle de Acesso", "teams_not_found": "Equipes não encontradas", "text": "Texto", "time": "tempo", @@ -792,6 +792,29 @@ "secret": "Segredo", "unable_to_delete_api_key": "Não foi possível deletar a Chave API" }, + "api_keys": { + "access_control": "Controle de Acesso", + "add_api_key": "Adicionar Chave API", + "add_env_api_key": "Adicionar chave de API {environmentType}", + "api_key": "Chave de API", + "api_key_copied_to_clipboard": "Chave da API copiada para a área de transferência", + "api_key_created": "Chave da API criada", + "api_key_deleted": "Chave da API deletada", + "api_key_label": "Rótulo da Chave API", + "api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", + "dev_api_keys": "Chaves do Ambiente de Desenvolvimento", + "dev_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Desenvolvimento.", + "duplicate_access": "Acesso duplicado ao projeto não permitido", + "duplicate_permissions": "Permissões duplicadas não são permitidas", + "no_api_keys_yet": "Você ainda não tem nenhuma chave de API", + "organization_access": "Acesso à Organização", + "permissions": "Permissões", + "prod_api_keys": "Chaves do Ambiente de Produção", + "prod_api_keys_description": "Adicionar e remover chaves de API para seu ambiente de Produção.", + "project_access": "Acesso ao Projeto", + "secret": "Segredo", + "unable_to_delete_api_key": "Não foi possível deletar a Chave API" + }, "app-connection": { "api_host_description": "Essa é a URL do seu backend do Formbricks.", "app_connection": "Conexão do App", @@ -988,6 +1011,12 @@ "with_the_formbricks_sdk": "com o SDK do Formbricks." }, "settings": { + "api_keys": { + "add_api_key": "Adicionar chave de API", + "add_permission": "Adicionar permissão", + "api_keys_description": "Gerencie chaves de API para acessar as APIs de gerenciamento do Formbricks", + "only_organization_owners_and_managers_can_manage_api_keys": "Apenas proprietários e gerentes da organização podem gerenciar chaves de API" + }, "billing": { "10000_monthly_responses": "10000 Respostas Mensais", "1500_monthly_responses": "1500 Respostas Mensais", diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index 98860b6f64..8c7a7c39be 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -372,7 +372,7 @@ "team": "Equipa", "team_access": "Acesso da Equipa", "team_name": "Nome da equipa", - "teams": "Equipas", + "teams": "Controlo de Acesso", "teams_not_found": "Equipas não encontradas", "text": "Texto", "time": "Tempo", @@ -792,6 +792,29 @@ "secret": "Segredo", "unable_to_delete_api_key": "Não é possível eliminar a chave API" }, + "api_keys": { + "access_control": "Controlo de Acesso", + "add_api_key": "Adicionar Chave API", + "add_env_api_key": "Adicionar Chave API {environmentType}", + "api_key": "Chave API", + "api_key_copied_to_clipboard": "Chave API copiada para a área de transferência", + "api_key_created": "Chave API criada", + "api_key_deleted": "Chave API eliminada", + "api_key_label": "Etiqueta da Chave API", + "api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", + "dev_api_keys": "Chaves de Ambiente de Desenvolvimento", + "dev_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Desenvolvimento.", + "duplicate_access": "Acesso duplicado ao projeto não permitido", + "duplicate_permissions": "Permissões duplicadas não são permitidas", + "no_api_keys_yet": "Ainda não tem nenhuma chave API", + "organization_access": "Acesso à Organização", + "permissions": "Permissões", + "prod_api_keys": "Chaves de Ambiente de Produção", + "prod_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Produção.", + "project_access": "Acesso ao Projeto", + "secret": "Segredo", + "unable_to_delete_api_key": "Não é possível eliminar a chave API" + }, "app-connection": { "api_host_description": "Este é o URL do seu backend Formbricks.", "app_connection": "Ligação de Aplicação", @@ -988,6 +1011,12 @@ "with_the_formbricks_sdk": "com o SDK Formbricks" }, "settings": { + "api_keys": { + "add_api_key": "Adicionar chave API", + "add_permission": "Adicionar permissão", + "api_keys_description": "Gerir chaves API para aceder às APIs de gestão do Formbricks", + "only_organization_owners_and_managers_can_manage_api_keys": "Apenas os proprietários e gestores da organização podem gerir chaves API" + }, "billing": { "10000_monthly_responses": "10000 Respostas Mensais", "1500_monthly_responses": "1500 Respostas Mensais", diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index 7a155c79a2..9526ca705e 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -372,7 +372,7 @@ "team": "團隊", "team_access": "團隊存取權限", "team_name": "團隊名稱", - "teams": "團隊", + "teams": "存取控制", "teams_not_found": "找不到團隊", "text": "文字", "time": "時間", @@ -792,6 +792,29 @@ "secret": "密碼", "unable_to_delete_api_key": "無法刪除 API 金鑰" }, + "api_keys": { + "access_control": "存取控制", + "add_api_key": "新增 API 金鑰", + "add_env_api_key": "新增 '{'environmentType'}' API 金鑰", + "api_key": "API 金鑰", + "api_key_copied_to_clipboard": "API 金鑰已複製到剪貼簿", + "api_key_created": "API 金鑰已建立", + "api_key_deleted": "API 金鑰已刪除", + "api_key_label": "API 金鑰標籤", + "api_key_security_warning": "為安全起見,API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。", + "dev_api_keys": "開發環境金鑰", + "dev_api_keys_description": "為您的開發環境新增和移除 API 金鑰。", + "duplicate_access": "不允許重複的 project 存取", + "duplicate_permissions": "不允許重複權限", + "no_api_keys_yet": "您還沒有任何 API 金鑰", + "organization_access": "組織 Access", + "permissions": "權限", + "prod_api_keys": "生產環境金鑰", + "prod_api_keys_description": "為您的生產環境新增和移除 API 金鑰。", + "project_access": "專案存取", + "secret": "密碼", + "unable_to_delete_api_key": "無法刪除 API 金鑰" + }, "app-connection": { "api_host_description": "這是您 Formbricks 後端的網址。", "app_connection": "應用程式連線", @@ -988,6 +1011,12 @@ "with_the_formbricks_sdk": "使用 Formbricks SDK" }, "settings": { + "api_keys": { + "add_api_key": "新增 API 金鑰", + "add_permission": "新增權限", + "api_keys_description": "管理 API 金鑰以存取 Formbricks 管理 API", + "only_organization_owners_and_managers_can_manage_api_keys": "只有組織擁有者和管理員才能管理 API 金鑰" + }, "billing": { "10000_monthly_responses": "10000 個每月回應", "1500_monthly_responses": "1500 個每月回應", diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts index 19133f5d48..0bdbd40112 100644 --- a/packages/lib/response/service.ts +++ b/packages/lib/response/service.ts @@ -92,7 +92,7 @@ export const responseSelection = { }, } satisfies Prisma.ResponseSelect; -const getResponseContact = ( +export const getResponseContact = ( responsePrisma: Prisma.ResponseGetPayload<{ select: typeof responseSelection }> ): TResponseContact | null => { if (!responsePrisma.contact) return null; diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index aea8f2ce76..593c4456b8 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -135,7 +135,7 @@ const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: Act } }; -const handleTriggerUpdates = ( +export const handleTriggerUpdates = ( updatedTriggers: TSurvey["triggers"], currentTriggers: TSurvey["triggers"], actionClasses: ActionClass[] diff --git a/packages/lib/vitestSetup.ts b/packages/lib/vitestSetup.ts index d4fefc99a3..1856498783 100644 --- a/packages/lib/vitestSetup.ts +++ b/packages/lib/vitestSetup.ts @@ -1,8 +1,15 @@ // mock these globally used functions import "@testing-library/jest-dom/vitest"; +import ResizeObserver from "resize-observer-polyfill"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { ValidationError } from "@formbricks/types/errors"; +// Make ResizeObserver available globally (Vitest/Jest environment) +// This is used by radix-ui +if (!global.ResizeObserver) { + global.ResizeObserver = ResizeObserver; +} + // mock react toast vi.mock("react-hot-toast", () => ({ diff --git a/packages/react-native/src/components/survey-web-view.tsx b/packages/react-native/src/components/survey-web-view.tsx index 7fe6a4fdfa..8395007f45 100644 --- a/packages/react-native/src/components/survey-web-view.tsx +++ b/packages/react-native/src/components/survey-web-view.tsx @@ -1,13 +1,13 @@ /* eslint-disable no-console -- debugging*/ -import React, { type JSX, useEffect, useRef, useState } from "react"; -import { Modal } from "react-native"; -import { WebView, type WebViewMessageEvent } from "react-native-webview"; import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { filterSurveys, getLanguageCode, getStyling } from "@/lib/common/utils"; import { SurveyStore } from "@/lib/survey/store"; import { type TEnvironmentStateSurvey, type TUserState, ZJsRNWebViewOnMessageData } from "@/types/config"; import type { SurveyContainerProps } from "@/types/survey"; +import React, { type JSX, useEffect, useRef, useState } from "react"; +import { Modal } from "react-native"; +import { WebView, type WebViewMessageEvent } from "react-native-webview"; const appConfig = RNConfig.getInstance(); const logger = Logger.getInstance(); diff --git a/packages/react-native/src/lib/survey/action.ts b/packages/react-native/src/lib/survey/action.ts index cb5a4bf325..35d24d0f8a 100644 --- a/packages/react-native/src/lib/survey/action.ts +++ b/packages/react-native/src/lib/survey/action.ts @@ -1,10 +1,10 @@ -import { fetch } from "@react-native-community/netinfo"; import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { shouldDisplayBasedOnPercentage } from "@/lib/common/utils"; import { SurveyStore } from "@/lib/survey/store"; import type { TEnvironmentStateSurvey } from "@/types/config"; import { type InvalidCodeError, type NetworkError, type Result, err, okVoid } from "@/types/error"; +import { fetch } from "@react-native-community/netinfo"; /** * Triggers the display of a survey if it meets the display percentage criteria diff --git a/packages/react-native/src/lib/survey/tests/action.test.ts b/packages/react-native/src/lib/survey/tests/action.test.ts index 9419fc6c37..e015737115 100644 --- a/packages/react-native/src/lib/survey/tests/action.test.ts +++ b/packages/react-native/src/lib/survey/tests/action.test.ts @@ -1,10 +1,10 @@ -import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; import { RNConfig } from "@/lib/common/config"; import { Logger } from "@/lib/common/logger"; import { shouldDisplayBasedOnPercentage } from "@/lib/common/utils"; import { track, trackAction, triggerSurvey } from "@/lib/survey/action"; import { SurveyStore } from "@/lib/survey/store"; import { type TEnvironmentStateSurvey } from "@/types/config"; +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("@/lib/common/config", () => ({ RNConfig: { diff --git a/packages/types/api-key.ts b/packages/types/api-key.ts new file mode 100644 index 0000000000..fbcb5a4df1 --- /dev/null +++ b/packages/types/api-key.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +enum OrganizationAccessType { + Read = "read", + Write = "write", +} + +enum OrganizationAccess { + AccessControl = "accessControl", +} + +const organizationAccessTypeValues = Object.values(OrganizationAccessType); + +const organizationAccessTypeShape = organizationAccessTypeValues.reduce>( + (acc, enumKey) => { + acc[enumKey] = z.boolean(); + return acc; + }, + {} +); + +const organizationAccessValues = Object.values(OrganizationAccess); + +export const ZOrganizationAccess = z.object( + organizationAccessValues.reduce>>>( + (acc, access) => { + acc[access] = z.object(organizationAccessTypeShape).strict(); + return acc; + }, + {} + ) +); + +export type TOrganizationAccess = z.infer; diff --git a/packages/types/auth.ts b/packages/types/auth.ts index a8dc9a7f84..aef9184868 100644 --- a/packages/types/auth.ts +++ b/packages/types/auth.ts @@ -1,3 +1,4 @@ +import { ApiKeyPermission } from "@prisma/client"; import { z } from "zod"; import { ZUser } from "./user"; @@ -5,10 +6,19 @@ export const ZAuthSession = z.object({ user: ZUser, }); +export const ZAPIKeyEnvironmentPermission = z.object({ + environmentId: z.string(), + permission: z.nativeEnum(ApiKeyPermission), +}); + +export type TAPIKeyEnvironmentPermission = z.infer; + export const ZAuthenticationApiKey = z.object({ type: z.literal("apiKey"), - environmentId: z.string(), + environmentPermissions: z.array(ZAPIKeyEnvironmentPermission), hashedApiKey: z.string(), + apiKeyId: z.string().optional(), + organizationId: z.string().optional(), }); export type TAuthSession = z.infer; diff --git a/packages/types/package.json b/packages/types/package.json index c4ecad7b87..9eb7766ea2 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -13,6 +13,7 @@ "@formbricks/database": "workspace:*" }, "dependencies": { + "@prisma/client": "6.0.1", "zod": "3.24.1", "zod-openapi": "4.2.4" } diff --git a/packages/types/surveys/types.ts b/packages/types/surveys/types.ts index f673bdb2d5..4e8a2eec37 100644 --- a/packages/types/surveys/types.ts +++ b/packages/types/surveys/types.ts @@ -2359,6 +2359,29 @@ export const ZSurveyCreateInput = makeSchemaOptional(ZSurvey.innerType()) export type TSurvey = z.infer; +export const ZSurveyCreateInputWithEnvironmentId = makeSchemaOptional(ZSurvey.innerType()) + .omit({ + id: true, + createdAt: true, + updatedAt: true, + projectOverwrites: true, + languages: true, + followUps: true, + }) + .extend({ + name: z.string(), // Keep name required + environmentId: z.string(), + questions: ZSurvey.innerType().shape.questions, // Keep questions required and with its original validation + languages: z.array(ZSurveyLanguage).default([]), + welcomeCard: ZSurveyWelcomeCard.default({ + enabled: false, + }), + endings: ZSurveyEndings.default([]), + type: ZSurveyType.default("link"), + followUps: z.array(ZSurveyFollowUp.omit({ createdAt: true, updatedAt: true })).default([]), + }) + .superRefine(ZSurvey._def.effect.type === "refinement" ? ZSurvey._def.effect.refinement : () => null); + export interface TSurveyDates { createdAt: TSurvey["createdAt"]; updatedAt: TSurvey["updatedAt"]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81f25c306e..3b48d2a222 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -598,6 +598,9 @@ importers: '@vitest/coverage-v8': specifier: 2.1.8 version: 2.1.8(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + resize-observer-polyfill: + specifier: 1.5.1 + version: 1.5.1 vite: specifier: 6.2.3 version: 6.2.3(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) @@ -1041,6 +1044,9 @@ importers: packages/types: dependencies: + '@prisma/client': + specifier: 6.0.1 + version: 6.0.1(prisma@6.5.0(typescript@5.8.2)) zod: specifier: 3.24.1 version: 3.24.1 From f32401afd68f460bbb8fd261ba1dec3bb16d9995 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 4 Apr 2025 10:40:21 +0900 Subject: [PATCH 140/411] chore: update vite & vitest dependency versions (#5217) --- .github/workflows/sonarqube.yml | 4 +- apps/demo/package.json | 2 +- apps/web/package.json | 12 +- packages/api/package.json | 4 +- packages/js-core/package.json | 10 +- .../lib/common/tests/event-listeners.test.ts | 18 +- packages/js-core/vite.config.ts | 8 +- packages/lib/messages/de-DE.json | 24 - packages/lib/messages/en-US.json | 24 - packages/lib/messages/fr-FR.json | 24 - packages/lib/messages/pt-BR.json | 24 - packages/lib/messages/pt-PT.json | 24 - packages/lib/messages/zh-Hant-TW.json | 24 - packages/lib/package.json | 4 +- packages/react-native/package.json | 8 +- packages/react-native/vite.config.ts | 8 +- packages/surveys/package.json | 6 +- pnpm-lock.yaml | 2280 ++++------------- 18 files changed, 594 insertions(+), 1914 deletions(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index b7dfe776df..2657ee8402 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -23,10 +23,10 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Setup Node.js 20.x + - name: Setup Node.js 22.x uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af with: - node-version: 20.x + node-version: 22.x - name: Install pnpm uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 diff --git a/apps/demo/package.json b/apps/demo/package.json index 476d2a833d..944f73072c 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -15,7 +15,7 @@ "@tailwindcss/forms": "0.5.9", "lucide-react": "0.486.0", "next": "15.2.4", - "postcss": "8.4.49", + "postcss": "8.5.3", "react": "19.0.0", "react-dom": "19.0.0", "tailwindcss": "3.4.16" diff --git a/apps/web/package.json b/apps/web/package.json index 6d250ecc50..6a5035da23 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@ai-sdk/azure": "1.1.9", - "@boxyhq/saml-jackson": "1.37.1", + "@boxyhq/saml-jackson": "1.44.0", "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", @@ -113,7 +113,7 @@ "optional": "0.1.4", "otplib": "12.0.1", "papaparse": "5.4.1", - "postcss": "8.4.49", + "postcss": "8.5.3", "posthog-js": "1.200.2", "prismjs": "1.30.0", "qr-code-styling": "1.9.1", @@ -154,11 +154,11 @@ "@types/papaparse": "5.3.15", "@types/qrcode": "1.5.5", "@types/testing-library__react": "10.2.0", - "@vitest/coverage-v8": "2.1.8", + "@vitest/coverage-v8": "3.1.1", + "vite": "6.2.4", "resize-observer-polyfill": "1.5.1", - "vite": "6.2.3", "vite-tsconfig-paths": "5.1.4", - "vitest": "3.0.7", - "vitest-mock-extended": "2.0.2" + "vitest": "3.1.1", + "vitest-mock-extended": "3.0.1" } } diff --git a/packages/api/package.json b/packages/api/package.json index 844d2323c4..8df82a382f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -40,8 +40,8 @@ "@rollup/plugin-inject": "5.0.5", "buffer": "6.0.3", "terser": "5.37.0", - "vite": "6.0.12", - "vite-plugin-dts": "4.3.0", + "vite": "6.2.4", + "vite-plugin-dts": "4.5.3", "vite-plugin-node-polyfills": "0.22.0" } } diff --git a/packages/js-core/package.json b/packages/js-core/package.json index d929e164a8..5d82e25fa1 100644 --- a/packages/js-core/package.json +++ b/packages/js-core/package.json @@ -46,10 +46,10 @@ "@formbricks/api": "workspace:*", "@formbricks/config-typescript": "workspace:*", "@formbricks/eslint-config": "workspace:*", - "@vitest/coverage-v8": "3.0.7", - "terser": "5.37.0", - "vite": "6.0.12", - "vite-plugin-dts": "4.3.0", - "vitest": "3.0.6" + "@vitest/coverage-v8": "3.1.1", + "terser": "5.39.0", + "vite": "6.2.4", + "vite-plugin-dts": "4.5.3", + "vitest": "3.1.1" } } diff --git a/packages/js-core/src/lib/common/tests/event-listeners.test.ts b/packages/js-core/src/lib/common/tests/event-listeners.test.ts index 4ae7e9a83c..18c5755353 100644 --- a/packages/js-core/src/lib/common/tests/event-listeners.test.ts +++ b/packages/js-core/src/lib/common/tests/event-listeners.test.ts @@ -1,5 +1,4 @@ // event-listeners.test.ts -import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; import { addCleanupEventListeners, addEventListeners, @@ -9,6 +8,7 @@ import { import * as environmentState from "@/lib/environment/state"; import * as pageUrlEventListeners from "@/lib/survey/no-code-action"; import * as userState from "@/lib/user/state"; +import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; // 1) Mock all the imported dependencies @@ -53,8 +53,7 @@ describe("event-listeners file", () => { // addEventListeners // --------------------------------------------------------------------------- test("addEventListeners calls each imported add* function", () => { - addEventListeners(); - + // Ensure the mocks are set up before calling the functions const mockEnvAdd = vi.spyOn(environmentState, "addEnvironmentStateExpiryCheckListener"); const mockUserAdd = vi.spyOn(userState, "addUserStateExpiryCheckListener"); const mockPageUrlAdd = vi.spyOn(pageUrlEventListeners, "addPageUrlEventListeners"); @@ -62,6 +61,10 @@ describe("event-listeners file", () => { const mockExitAdd = vi.spyOn(pageUrlEventListeners, "addExitIntentListener"); const mockScrollAdd = vi.spyOn(pageUrlEventListeners, "addScrollDepthListener"); + // Call the function after setting up the spies + addEventListeners(); + + // Assertions expect(mockEnvAdd).toHaveBeenCalled(); expect(mockUserAdd).toHaveBeenCalled(); expect(mockPageUrlAdd).toHaveBeenCalled(); @@ -111,8 +114,7 @@ describe("event-listeners file", () => { // removeAllEventListeners // --------------------------------------------------------------------------- test("removeAllEventListeners calls all the remove/clear functions", () => { - removeAllEventListeners(); - + // Ensure the mocks are set up before calling the function const mockEnvClear = vi.spyOn(environmentState, "clearEnvironmentStateExpiryCheckListener"); const mockUserClear = vi.spyOn(userState, "clearUserStateExpiryCheckListener"); const mockPageUrlRemove = vi.spyOn(pageUrlEventListeners, "removePageUrlEventListeners"); @@ -120,10 +122,12 @@ describe("event-listeners file", () => { const mockExitRemove = vi.spyOn(pageUrlEventListeners, "removeExitIntentListener"); const mockScrollRemove = vi.spyOn(pageUrlEventListeners, "removeScrollDepthListener"); - // environment & user state + // Call the function after setting up the spies + removeAllEventListeners(); + + // Assertions expect(mockEnvClear).toHaveBeenCalled(); expect(mockUserClear).toHaveBeenCalled(); - // pageUrl/click/exit/scroll expect(mockPageUrlRemove).toHaveBeenCalled(); expect(mockClickRemove).toHaveBeenCalled(); expect(mockExitRemove).toHaveBeenCalled(); diff --git a/packages/js-core/vite.config.ts b/packages/js-core/vite.config.ts index 6f3b8f1035..667ed7d68c 100644 --- a/packages/js-core/vite.config.ts +++ b/packages/js-core/vite.config.ts @@ -1,13 +1,9 @@ import { resolve } from "path"; -import { InlineConfig, UserConfig, defineConfig } from "vite"; +import { defineConfig } from "vite"; import dts from "vite-plugin-dts"; import webPackageJson from "../../apps/web/package.json"; import { copyCompiledAssetsPlugin } from "../vite-plugins/copy-compiled-assets"; -interface VitestConfigExport extends UserConfig { - test: InlineConfig; -} - const config = () => { return defineConfig({ resolve: { @@ -51,7 +47,7 @@ const config = () => { include: ["src/lib/**/*.ts"], }, }, - } as VitestConfigExport); + }); }; export default config; diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index f4e62b6669..2452026583 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -775,42 +775,18 @@ "zapier_integration_description": "Integriere Formbricks mit über 5000 Apps über Zapier" }, "project": { - "api-keys": { - "add_api_key": "API-Schlüssel hinzufügen", - "add_env_api_key": "{environmentType} API-Schlüssel hinzufügen", - "api_key": "API-Schlüssel", - "api_key_copied_to_clipboard": "API-Schlüssel in die Zwischenablage kopiert", - "api_key_created": "API-Schlüssel erstellt", - "api_key_deleted": "API-Schlüssel gelöscht", - "api_key_label": "API-Schlüssel Label", - "api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.", - "dev_api_keys": "API-Schlüssel (Dev)", - "dev_api_keys_description": "API-Schlüssel für deine Entwicklungsumgebung hinzufügen und entfernen.", - "no_api_keys_yet": "Du hast noch keine API-Schlüssel", - "prod_api_keys": "API-Schlüssel (Prod)", - "prod_api_keys_description": "API-Schlüssel für deine Produktionsumgebung hinzufügen und entfernen.", - "secret": "Geheimnis", - "unable_to_delete_api_key": "API-Schlüssel kann nicht gelöscht werden" - }, "api_keys": { - "access_control": "Zugriffskontrolle", "add_api_key": "API-Schlüssel hinzufügen", - "add_env_api_key": "{environmentType} API-Schlüssel hinzufügen", "api_key": "API-Schlüssel", "api_key_copied_to_clipboard": "API-Schlüssel in die Zwischenablage kopiert", "api_key_created": "API-Schlüssel erstellt", "api_key_deleted": "API-Schlüssel gelöscht", "api_key_label": "API-Schlüssel Label", "api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.", - "dev_api_keys": "API-Schlüssel (Dev)", - "dev_api_keys_description": "API-Schlüssel für deine Entwicklungsumgebung hinzufügen und entfernen.", "duplicate_access": "Doppelter Projektzugriff nicht erlaubt", - "duplicate_permissions": "Doppelte Berechtigungen sind nicht erlaubt", "no_api_keys_yet": "Du hast noch keine API-Schlüssel", "organization_access": "Organisationszugang", "permissions": "Berechtigungen", - "prod_api_keys": "API-Schlüssel (Prod)", - "prod_api_keys_description": "API-Schlüssel für deine Produktionsumgebung hinzufügen und entfernen.", "project_access": "Projektzugriff", "secret": "Geheimnis", "unable_to_delete_api_key": "API-Schlüssel kann nicht gelöscht werden" diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 0cc257ff99..b08813f57f 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -775,42 +775,18 @@ "zapier_integration_description": "Integrate Formbricks with 5000+ apps via Zapier" }, "project": { - "api-keys": { - "add_api_key": "Add API Key", - "add_env_api_key": "Add {environmentType} API Key", - "api_key": "API Key", - "api_key_copied_to_clipboard": "API key copied to clipboard", - "api_key_created": "API key created", - "api_key_deleted": "API Key deleted", - "api_key_label": "API Key Label", - "api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.", - "dev_api_keys": "Development Env Keys", - "dev_api_keys_description": "Add and remove API keys for your Development environment.", - "no_api_keys_yet": "You don't have any API keys yet", - "prod_api_keys": "Production Env Keys", - "prod_api_keys_description": "Add and remove API keys for your Production environment.", - "secret": "Secret", - "unable_to_delete_api_key": "Unable to delete API Key" - }, "api_keys": { - "access_control": "Access Control", "add_api_key": "Add API Key", - "add_env_api_key": "Add {environmentType} API Key", "api_key": "API Key", "api_key_copied_to_clipboard": "API key copied to clipboard", "api_key_created": "API key created", "api_key_deleted": "API Key deleted", "api_key_label": "API Key Label", "api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.", - "dev_api_keys": "Development Env Keys", - "dev_api_keys_description": "Add and remove API keys for your Development environment.", "duplicate_access": "Duplicate project access not allowed", - "duplicate_permissions": "Duplicate permissions not allowed", "no_api_keys_yet": "You don't have any API keys yet", "organization_access": "Organization Access", "permissions": "Permissions", - "prod_api_keys": "Production Env Keys", - "prod_api_keys_description": "Add and remove API keys for your Production environment.", "project_access": "Project Access", "secret": "Secret", "unable_to_delete_api_key": "Unable to delete API Key" diff --git a/packages/lib/messages/fr-FR.json b/packages/lib/messages/fr-FR.json index 68578df2a2..a32ed280b9 100644 --- a/packages/lib/messages/fr-FR.json +++ b/packages/lib/messages/fr-FR.json @@ -775,42 +775,18 @@ "zapier_integration_description": "Intégrez Formbricks avec plus de 5000 applications via Zapier." }, "project": { - "api-keys": { - "add_api_key": "Ajouter une clé API", - "add_env_api_key": "Ajouter la clé API {environmentType}", - "api_key": "Clé API", - "api_key_copied_to_clipboard": "Clé API copiée dans le presse-papiers", - "api_key_created": "Clé API créée", - "api_key_deleted": "Clé API supprimée", - "api_key_label": "Étiquette de clé API", - "api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.", - "dev_api_keys": "Clés de l'environnement de développement", - "dev_api_keys_description": "Ajoutez et supprimez des clés API pour votre environnement de développement.", - "no_api_keys_yet": "Vous n'avez pas encore de clés API.", - "prod_api_keys": "Clés de l'environnement de production", - "prod_api_keys_description": "Ajoutez et supprimez des clés API pour votre environnement de production.", - "secret": "Secret", - "unable_to_delete_api_key": "Impossible de supprimer la clé API" - }, "api_keys": { - "access_control": "Contrôle d'accès", "add_api_key": "Ajouter une clé API", - "add_env_api_key": "Ajouter la clé API {environmentType}", "api_key": "Clé API", "api_key_copied_to_clipboard": "Clé API copiée dans le presse-papiers", "api_key_created": "Clé API créée", "api_key_deleted": "Clé API supprimée", "api_key_label": "Étiquette de clé API", "api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.", - "dev_api_keys": "Clés de l'environnement de développement", - "dev_api_keys_description": "Ajoutez et supprimez des clés API pour votre environnement de développement.", "duplicate_access": "L'accès en double au projet n'est pas autorisé", - "duplicate_permissions": "Les autorisations en double ne sont pas autorisées", "no_api_keys_yet": "Vous n'avez pas encore de clés API.", "organization_access": "Accès à l'organisation", "permissions": "Permissions", - "prod_api_keys": "Clés de l'environnement de production", - "prod_api_keys_description": "Ajoutez et supprimez des clés API pour votre environnement de production.", "project_access": "Accès au projet", "secret": "Secret", "unable_to_delete_api_key": "Impossible de supprimer la clé API" diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index 80f02023e0..e31cef5e22 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -775,42 +775,18 @@ "zapier_integration_description": "Integrar o Formbricks com mais de 5000 apps via Zapier" }, "project": { - "api-keys": { - "add_api_key": "Adicionar Chave API", - "add_env_api_key": "Adicionar chave de API {environmentType}", - "api_key": "Chave de API", - "api_key_copied_to_clipboard": "Chave da API copiada para a área de transferência", - "api_key_created": "Chave da API criada", - "api_key_deleted": "Chave da API deletada", - "api_key_label": "Rótulo da Chave API", - "api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", - "dev_api_keys": "Chaves do Ambiente de Desenvolvimento", - "dev_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Desenvolvimento.", - "no_api_keys_yet": "Você ainda não tem nenhuma chave de API", - "prod_api_keys": "Chaves do Ambiente de Produção", - "prod_api_keys_description": "Adicionar e remover chaves de API para seu ambiente de Produção.", - "secret": "Segredo", - "unable_to_delete_api_key": "Não foi possível deletar a Chave API" - }, "api_keys": { - "access_control": "Controle de Acesso", "add_api_key": "Adicionar Chave API", - "add_env_api_key": "Adicionar chave de API {environmentType}", "api_key": "Chave de API", "api_key_copied_to_clipboard": "Chave da API copiada para a área de transferência", "api_key_created": "Chave da API criada", "api_key_deleted": "Chave da API deletada", "api_key_label": "Rótulo da Chave API", "api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", - "dev_api_keys": "Chaves do Ambiente de Desenvolvimento", - "dev_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Desenvolvimento.", "duplicate_access": "Acesso duplicado ao projeto não permitido", - "duplicate_permissions": "Permissões duplicadas não são permitidas", "no_api_keys_yet": "Você ainda não tem nenhuma chave de API", "organization_access": "Acesso à Organização", "permissions": "Permissões", - "prod_api_keys": "Chaves do Ambiente de Produção", - "prod_api_keys_description": "Adicionar e remover chaves de API para seu ambiente de Produção.", "project_access": "Acesso ao Projeto", "secret": "Segredo", "unable_to_delete_api_key": "Não foi possível deletar a Chave API" diff --git a/packages/lib/messages/pt-PT.json b/packages/lib/messages/pt-PT.json index 8c7a7c39be..88281dafad 100644 --- a/packages/lib/messages/pt-PT.json +++ b/packages/lib/messages/pt-PT.json @@ -775,42 +775,18 @@ "zapier_integration_description": "Integre o Formbricks com mais de 5000 apps via Zapier" }, "project": { - "api-keys": { - "add_api_key": "Adicionar Chave API", - "add_env_api_key": "Adicionar Chave API {environmentType}", - "api_key": "Chave API", - "api_key_copied_to_clipboard": "Chave API copiada para a área de transferência", - "api_key_created": "Chave API criada", - "api_key_deleted": "Chave API eliminada", - "api_key_label": "Etiqueta da Chave API", - "api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", - "dev_api_keys": "Chaves de Ambiente de Desenvolvimento", - "dev_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Desenvolvimento.", - "no_api_keys_yet": "Ainda não tem nenhuma chave API", - "prod_api_keys": "Chaves de Ambiente de Produção", - "prod_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Produção.", - "secret": "Segredo", - "unable_to_delete_api_key": "Não é possível eliminar a chave API" - }, "api_keys": { - "access_control": "Controlo de Acesso", "add_api_key": "Adicionar Chave API", - "add_env_api_key": "Adicionar Chave API {environmentType}", "api_key": "Chave API", "api_key_copied_to_clipboard": "Chave API copiada para a área de transferência", "api_key_created": "Chave API criada", "api_key_deleted": "Chave API eliminada", "api_key_label": "Etiqueta da Chave API", "api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.", - "dev_api_keys": "Chaves de Ambiente de Desenvolvimento", - "dev_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Desenvolvimento.", "duplicate_access": "Acesso duplicado ao projeto não permitido", - "duplicate_permissions": "Permissões duplicadas não são permitidas", "no_api_keys_yet": "Ainda não tem nenhuma chave API", "organization_access": "Acesso à Organização", "permissions": "Permissões", - "prod_api_keys": "Chaves de Ambiente de Produção", - "prod_api_keys_description": "Adicionar e remover chaves de API para o seu ambiente de Produção.", "project_access": "Acesso ao Projeto", "secret": "Segredo", "unable_to_delete_api_key": "Não é possível eliminar a chave API" diff --git a/packages/lib/messages/zh-Hant-TW.json b/packages/lib/messages/zh-Hant-TW.json index 9526ca705e..0e826379ae 100644 --- a/packages/lib/messages/zh-Hant-TW.json +++ b/packages/lib/messages/zh-Hant-TW.json @@ -775,42 +775,18 @@ "zapier_integration_description": "透過 Zapier 將 Formbricks 與 5000 多個應用程式整合" }, "project": { - "api-keys": { - "add_api_key": "新增 API 金鑰", - "add_env_api_key": "新增 '{'environmentType'}' API 金鑰", - "api_key": "API 金鑰", - "api_key_copied_to_clipboard": "API 金鑰已複製到剪貼簿", - "api_key_created": "API 金鑰已建立", - "api_key_deleted": "API 金鑰已刪除", - "api_key_label": "API 金鑰標籤", - "api_key_security_warning": "為安全起見,API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。", - "dev_api_keys": "開發環境金鑰", - "dev_api_keys_description": "為您的開發環境新增和移除 API 金鑰。", - "no_api_keys_yet": "您還沒有任何 API 金鑰", - "prod_api_keys": "生產環境金鑰", - "prod_api_keys_description": "為您的生產環境新增和移除 API 金鑰。", - "secret": "密碼", - "unable_to_delete_api_key": "無法刪除 API 金鑰" - }, "api_keys": { - "access_control": "存取控制", "add_api_key": "新增 API 金鑰", - "add_env_api_key": "新增 '{'environmentType'}' API 金鑰", "api_key": "API 金鑰", "api_key_copied_to_clipboard": "API 金鑰已複製到剪貼簿", "api_key_created": "API 金鑰已建立", "api_key_deleted": "API 金鑰已刪除", "api_key_label": "API 金鑰標籤", "api_key_security_warning": "為安全起見,API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。", - "dev_api_keys": "開發環境金鑰", - "dev_api_keys_description": "為您的開發環境新增和移除 API 金鑰。", "duplicate_access": "不允許重複的 project 存取", - "duplicate_permissions": "不允許重複權限", "no_api_keys_yet": "您還沒有任何 API 金鑰", "organization_access": "組織 Access", "permissions": "權限", - "prod_api_keys": "生產環境金鑰", - "prod_api_keys_description": "為您的生產環境新增和移除 API 金鑰。", "project_access": "專案存取", "secret": "密碼", "unable_to_delete_api_key": "無法刪除 API 金鑰" diff --git a/packages/lib/package.json b/packages/lib/package.json index a0adbbf86e..084b3e7a3d 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -47,7 +47,7 @@ "@types/ungap__structured-clone": "1.2.0", "dotenv": "16.4.7", "ts-node": "10.9.2", - "vitest": "2.1.9", - "vitest-mock-extended": "2.0.2" + "vitest": "3.1.1", + "vitest-mock-extended": "3.0.1" } } diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 9775359acf..28216c6b9e 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -48,13 +48,13 @@ "@formbricks/api": "workspace:*", "@formbricks/config-typescript": "workspace:*", "@types/react": "18.3.1", - "@vitest/coverage-v8": "3.0.4", + "@vitest/coverage-v8": "3.1.1", "react": "18.3.1", "react-native": "0.74.5", "terser": "5.37.0", - "vite": "6.0.12", - "vite-plugin-dts": "4.3.0", - "vitest": "3.0.5" + "vite": "6.2.4", + "vite-plugin-dts": "4.5.3", + "vitest": "3.1.1" }, "peerDependencies": { "@react-native-async-storage/async-storage": ">=2.1.0", diff --git a/packages/react-native/vite.config.ts b/packages/react-native/vite.config.ts index 43c4f79e60..b0fc7f72cf 100644 --- a/packages/react-native/vite.config.ts +++ b/packages/react-native/vite.config.ts @@ -1,11 +1,7 @@ import { resolve } from "node:path"; -import { type InlineConfig, type UserConfig, defineConfig } from "vite"; +import { type UserConfig, defineConfig } from "vite"; import dts from "vite-plugin-dts"; -interface VitestConfigExport extends UserConfig { - test: InlineConfig; -} - const config = (): UserConfig => { return defineConfig({ resolve: { @@ -45,7 +41,7 @@ const config = (): UserConfig => { include: ["src/lib/**/*.ts"], }, }, - } as VitestConfigExport); + }); }; export default config; diff --git a/packages/surveys/package.json b/packages/surveys/package.json index 53cbae38b2..e2934dfa24 100644 --- a/packages/surveys/package.json +++ b/packages/surveys/package.json @@ -46,14 +46,14 @@ "autoprefixer": "10.4.20", "concurrently": "9.1.0", "isomorphic-dompurify": "2.19.0", - "postcss": "8.4.49", + "postcss": "8.5.3", "preact": "10.25.2", "react-date-picker": "11.0.0", "serve": "14.2.4", "tailwindcss": "3.4.16", "terser": "5.37.0", - "vite": "6.0.12", - "vite-plugin-dts": "4.3.0", + "vite": "6.2.4", + "vite-plugin-dts": "4.5.3", "vite-tsconfig-paths": "5.1.4" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b48d2a222..3e42e74d42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,8 +55,8 @@ importers: specifier: 15.2.4 version: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) postcss: - specifier: 8.4.49 - version: 8.4.49 + specifier: 8.5.3 + version: 8.5.3 react: specifier: 19.0.0 version: 19.0.0 @@ -193,8 +193,8 @@ importers: specifier: 1.1.9 version: 1.1.9(zod@3.24.1) '@boxyhq/saml-jackson': - specifier: 1.37.1 - version: 1.37.1(aws-crt@1.25.3)(socks@2.8.4)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)) + specifier: 1.44.0 + version: 1.44.0(aws-crt@1.25.3)(socks@2.8.4)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)) '@dnd-kit/core': specifier: 6.3.1 version: 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -386,7 +386,7 @@ importers: version: 4.1.17(react@19.0.0)(zod@3.24.1) autoprefixer: specifier: 10.4.20 - version: 10.4.20(postcss@8.4.49) + version: 10.4.20(postcss@8.5.3) bcryptjs: specifier: 2.4.3 version: 2.4.3 @@ -478,8 +478,8 @@ importers: specifier: 5.4.1 version: 5.4.1 postcss: - specifier: 8.4.49 - version: 8.4.49 + specifier: 8.5.3 + version: 8.5.3 posthog-js: specifier: 1.200.2 version: 1.200.2 @@ -596,23 +596,23 @@ importers: specifier: 10.2.0 version: 10.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@vitest/coverage-v8': - specifier: 2.1.8 - version: 2.1.8(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 3.1.1 + version: 3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) resize-observer-polyfill: specifier: 1.5.1 version: 1.5.1 vite: - specifier: 6.2.3 - version: 6.2.3(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 6.2.4 + version: 6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.8.2)(vite@6.2.3(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + version: 5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) vitest: - specifier: 3.0.7 - version: 3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 3.1.1 + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) vitest-mock-extended: - specifier: 2.0.2 - version: 2.0.2(typescript@5.8.2)(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 3.0.1 + version: 3.0.1(typescript@5.8.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) packages/api: devDependencies: @@ -635,14 +635,14 @@ importers: specifier: 5.37.0 version: 5.37.0 vite: - specifier: 6.0.12 - version: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 6.2.4 + version: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-dts: - specifier: 4.3.0 - version: 4.3.0(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 4.5.3 + version: 4.5.3(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) vite-plugin-node-polyfills: specifier: 0.22.0 - version: 0.22.0(rollup@4.37.0)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + version: 0.22.0(rollup@4.37.0)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) packages/config-eslint: devDependencies: @@ -786,20 +786,20 @@ importers: specifier: workspace:* version: link:../config-eslint '@vitest/coverage-v8': - specifier: 3.0.7 - version: 3.0.7(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 3.1.1 + version: 3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) terser: - specifier: 5.37.0 - version: 5.37.0 + specifier: 5.39.0 + version: 5.39.0 vite: - specifier: 6.0.12 - version: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 6.2.4 + version: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-dts: - specifier: 4.3.0 - version: 4.3.0(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 4.5.3 + version: 4.5.3(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) vitest: - specifier: 3.0.6 - version: 3.0.6(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 3.1.1 + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) packages/lib: dependencies: @@ -895,11 +895,11 @@ importers: specifier: 10.9.2 version: 10.9.2(@types/node@22.10.2)(typescript@5.8.2) vitest: - specifier: 2.1.9 - version: 2.1.9(@types/node@22.10.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0) + specifier: 3.1.1 + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) vitest-mock-extended: - specifier: 2.0.2 - version: 2.0.2(typescript@5.8.2)(vitest@2.1.9(@types/node@22.10.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)) + specifier: 3.0.1 + version: 3.0.1(typescript@5.8.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) packages/logger: dependencies: @@ -954,8 +954,8 @@ importers: specifier: 18.3.1 version: 18.3.1 '@vitest/coverage-v8': - specifier: 3.0.4 - version: 3.0.4(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 3.1.1 + version: 3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) react: specifier: 18.3.1 version: 18.3.1 @@ -966,14 +966,14 @@ importers: specifier: 5.37.0 version: 5.37.0 vite: - specifier: 6.0.12 - version: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 6.2.4 + version: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-dts: - specifier: 4.3.0 - version: 4.3.0(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 4.5.3 + version: 4.5.3(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) vitest: - specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 3.1.1 + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) packages/surveys: dependencies: @@ -1001,13 +1001,13 @@ importers: version: link:../types '@preact/preset-vite': specifier: 2.9.3 - version: 2.9.3(@babel/core@7.26.0)(preact@10.25.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + version: 2.9.3(@babel/core@7.26.0)(preact@10.25.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) '@types/react': specifier: 19.0.1 version: 19.0.1 autoprefixer: specifier: 10.4.20 - version: 10.4.20(postcss@8.4.49) + version: 10.4.20(postcss@8.5.3) concurrently: specifier: 9.1.0 version: 9.1.0 @@ -1015,8 +1015,8 @@ importers: specifier: 2.19.0 version: 2.19.0 postcss: - specifier: 8.4.49 - version: 8.4.49 + specifier: 8.5.3 + version: 8.5.3 preact: specifier: 10.25.2 version: 10.25.2 @@ -1033,14 +1033,14 @@ importers: specifier: 5.37.0 version: 5.37.0 vite: - specifier: 6.0.12 - version: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + specifier: 6.2.4 + version: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) vite-plugin-dts: - specifier: 4.3.0 - version: 4.3.0(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: 4.5.3 + version: 4.5.3(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.8.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + version: 5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) packages/types: dependencies: @@ -1170,12 +1170,12 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-cognito-identity@3.741.0': - resolution: {integrity: sha512-zd1HMnBi/hSGEBHf1qEbjAZSynDWv6XUDW8dFLHaXJCFZiTJ0YLTxxKTGLHmqg969OOHLC7YSGoIiaF1Cpu9Mg==} + '@aws-sdk/client-cognito-identity@3.774.0': + resolution: {integrity: sha512-lOHskgBOvufIhg/RB+MoNQnoNwvOrInXH9J7zuGcrYHsj8yOKUc/hZbT25452dlNb9NXI6ZjZVSIsYw7uqd3Bg==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-dynamodb@3.741.0': - resolution: {integrity: sha512-D7/9QLyPWab5LM0X3R/0qTbGvUHY0Z+VAmQuxwdcGSo3497VwkRi97nm3jVRgjXLQibaPq05AXtugSjIj1/OJA==} + '@aws-sdk/client-dynamodb@3.774.0': + resolution: {integrity: sha512-l1Esw75XLKPVo77z7SukB/wxdtlnhafPNbkvXvEu1H8gJw8CmE34b5QlBzhVuRk/O5hzOpiRMQxXxNSRMdoMSQ==} engines: {node: '>=18.0.0'} '@aws-sdk/client-s3@3.741.0': @@ -1186,6 +1186,10 @@ packages: resolution: {integrity: sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==} engines: {node: '>=18.0.0'} + '@aws-sdk/client-sso@3.774.0': + resolution: {integrity: sha512-bN+wd2gpTq+DNJ/fZdam/mX6K3TcVdZBIvxaVtg+imep6xAuRukdFhsoG0cDzk96+WHPCOhkyi+6lFljCof43Q==} + engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.734.0': resolution: {integrity: sha512-SxnDqf3vobdm50OLyAKfqZetv6zzwnSqwIwd3jrbopxxHKqNIM/I0xcYjD6Tn+mPig+u7iRKb9q3QnEooFTlmg==} engines: {node: '>=18.0.0'} @@ -1194,34 +1198,58 @@ packages: resolution: {integrity: sha512-JDkAAlPyGWMX42L4Cv8mxybwHTOoFweNbNrOc5oQJhFxZAe1zkW4uLTEfr79vYhnXCFbThCyPpBotmo3U2vULA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-cognito-identity@3.741.0': - resolution: {integrity: sha512-EoEO1hxe9WToWvrknnHHIb/N5HQoIj53JAbdRlhDRjedKr7nQu8ELVGn4s0UkXuZc1CAUMlvwdiZpr3NxhcofA==} + '@aws-sdk/credential-provider-cognito-identity@3.774.0': + resolution: {integrity: sha512-xK1qiXH6RId+jTEPSWjRvnc47xAtGyAp2qXIQmCXWtNgoPRRsdvHAVHwkI3ND73Ze+rdpkTGK5Bbl7ZkBdQEDQ==} engines: {node: '>=18.0.0'} '@aws-sdk/credential-provider-env@3.734.0': resolution: {integrity: sha512-gtRkzYTGafnm1FPpiNO8VBmJrYMoxhDlGPYDVcijzx3DlF8dhWnowuSBCxLSi+MJMx5hvwrX2A+e/q0QAeHqmw==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.774.0': + resolution: {integrity: sha512-FkSDBi9Ly0bmzyrMDeqQq1lGsFMrrd/bIB3c9VD4Llh0sPLxB/DU31+VTPTuQ0pBPz4sX5Vay6tLy43DStzcFQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.734.0': resolution: {integrity: sha512-JFSL6xhONsq+hKM8xroIPhM5/FOhiQ1cov0lZxhzZWj6Ai3UAjucy3zyIFDr9MgP1KfCYNdvyaUq9/o+HWvEDg==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.774.0': + resolution: {integrity: sha512-iurWGQColf52HpHeHCQs/LnSjZ0Ufq3VtSQx/6QdZwIhmgbbqvGMAaBJg41SQjWhpqdufE96HzcaCJw/lnCefQ==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.741.0': resolution: {integrity: sha512-/XvnVp6zZXsyUlP1FtmspcWnd+Z1u2WK0wwzTE/x277M0oIhAezCW79VmcY4jcDQbYH+qMbtnBexfwgFDARxQg==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.774.0': + resolution: {integrity: sha512-+AsJOX9pGsnGPAC8wQw7LAO8ZfXzjXTjJxSP1fvg04PX7OBk4zwhVaryH6pu5raan+9cVbfEO1Z7EEMdkweGQA==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.741.0': resolution: {integrity: sha512-iz/puK9CZZkZjrKXX2W+PaiewHtlcD7RKUIsw4YHFyb8lrOt7yTYpM6VjeI+T//1sozjymmAnnp1SST9TXApLQ==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.774.0': + resolution: {integrity: sha512-/t+TNhHNW6BNyf7Lgv6I0NUfFk6/dz4+6dUjopRxpDVJtp1YvNza0Zhl25ffRkqX4CKmuXyJYusDbbObcsncUA==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.734.0': resolution: {integrity: sha512-zvjsUo+bkYn2vjT+EtLWu3eD6me+uun+Hws1IyWej/fKFAqiBPwyeyCgU7qjkiPQSXqk1U9+/HG9IQ6Iiz+eBw==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.774.0': + resolution: {integrity: sha512-lycBRY1NeWa46LefN258m1MRVUPQgvf6TPA6ZYajyq6/dCr6BPeuUoUAyrzePTPlxV/M25YXNiyORHjjwlK0ug==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.734.0': resolution: {integrity: sha512-cCwwcgUBJOsV/ddyh1OGb4gKYWEaTeTsqaAK19hiNINfYV/DO9r4RMlnWAo84sSBfJuj9shUNsxzyoe6K7R92Q==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.774.0': + resolution: {integrity: sha512-j7vbGCWF6dVpd9qiT0PQGzY4NKf8KUa86sSoosGGbtu0dV9T/Y0s/fvPZ0F8ZyuPIKUMJaBpIJYZ/ECZRfT2mg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.734.0': resolution: {integrity: sha512-t4OSOerc+ppK541/Iyn1AS40+2vT/qE+MFMotFkhCgCJbApeRF2ozEdnDN6tGmnl4ybcUuxnp9JWLjwDVlR/4g==} engines: {node: '>=18.0.0'} @@ -1230,8 +1258,8 @@ packages: resolution: {integrity: sha512-kuE5Hdqm9xXdrYBWCU6l2aM3W3HBtZrIBgyf0y41LulJHwld1nvIySus/lILdzbipmUAv9FI07B8TF5y7p/aFA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-providers@3.741.0': - resolution: {integrity: sha512-X0R46k09GtfOaCwZei6atf5gxFhwuZl7p9T5/LWTNo7rUiAWPLpnxwfDfymHQqTH0u4ShZYCRDHEoOk06wKm6g==} + '@aws-sdk/credential-providers@3.774.0': + resolution: {integrity: sha512-RQH18YmoYGwzx7sNDn/xlyOJWBSj89LmaGJG0XkInTiAzJhM7gLtEktYd6omum+3yMxb22YzP1ihK4giP2hieA==} engines: {node: '>=18.0.0'} '@aws-sdk/endpoint-cache@3.723.0': @@ -1242,8 +1270,8 @@ packages: resolution: {integrity: sha512-etC7G18aF7KdZguW27GE/wpbrNmYLVT755EsFc8kXpZj8D6AFKxc7OuveinJmiy0bYXAMspJUWsF6CrGpOw6CQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-endpoint-discovery@3.734.0': - resolution: {integrity: sha512-hE3x9Sbqy64g/lcFIq7BF9IS1tSOyfBCyHf1xBgevWeFIDTWh647URuCNWoEwtw4HMEhO2MDUQcKf1PFh1dNDA==} + '@aws-sdk/middleware-endpoint-discovery@3.774.0': + resolution: {integrity: sha512-lXc8JR9e3LnESs5x3xyJPo0nM/SooPghEsGoAO1BeQ+SeaLiYU+XPP3x3n6JYqCmfbrUwvnwtSGU5YLNX/8xhg==} engines: {node: '>=18.0.0'} '@aws-sdk/middleware-expect-continue@3.734.0': @@ -1322,6 +1350,10 @@ packages: resolution: {integrity: sha512-2U6yWKrjWjZO8Y5SHQxkFvMVWHQWbS0ufqfAIBROqmIZNubOL7jXCiVdEFekz6MZ9LF2tvYGnOW4jX8OKDGfIw==} engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.774.0': + resolution: {integrity: sha512-DDERwCduWFFXj7gx3qvnaB8GlnCUpQ8ZA03qI4QFokWu3EyHNK+hjp3nN5Dg81fI0Z82LRe30Q2uDsLBwNCZDg==} + engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.734.0': resolution: {integrity: sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==} engines: {node: '>=18.0.0'} @@ -1330,11 +1362,11 @@ packages: resolution: {integrity: sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-dynamodb@3.741.0': - resolution: {integrity: sha512-UWLz1COTE+mj0pQr/AvRoaG/ADIK0Gym8ds7sR7xPZvCGqeoEl/rGQbCd64/B2AYPwe76OYtdvuu/0/M7y27vw==} + '@aws-sdk/util-dynamodb@3.774.0': + resolution: {integrity: sha512-0qo0X9YRz2Xo89oWbJN/vPZOfOcrjp2AP6NdUDbIUC5WMn9OLri/O7m5k4wG+q0pSfTi1EcUUtHpcix1bXx56w==} engines: {node: '>=18.0.0'} peerDependencies: - '@aws-sdk/client-dynamodb': ^3.741.0 + '@aws-sdk/client-dynamodb': ^3.774.0 '@aws-sdk/util-endpoints@3.734.0': resolution: {integrity: sha512-w2+/E88NUbqql6uCVAsmMxDQKu7vsKV0KqhlQb0lL+RCq4zy07yXYptVNs13qrnuTfyX7uPXkXrlugvK9R1Ucg==} @@ -2216,9 +2248,6 @@ packages: resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -2226,15 +2255,15 @@ packages: '@boxyhq/error-code-mnemonic@0.1.1': resolution: {integrity: sha512-NmO111OG8GQDE8W/+uSREb67YSqnY2N/tHykGeFoIZc9Leher+lW+jN4U1OXzlc66hwB8yO7WRu2cbYsAKsi9g==} - '@boxyhq/metrics@0.2.9': - resolution: {integrity: sha512-0TP7jDX8pMy/YrD+0Gy9QI4ZAYh94s4VsYoqSfKJiL06i79hlso+3qC399zLyaB59442FQAV6+x36ZEb9GTXng==} + '@boxyhq/metrics@0.2.10': + resolution: {integrity: sha512-jsBKS5fNEyxYyizhYWT3q1oQM3dE9VVL/rEBrdGZMHXQZHxKZeE1Sg5wrfT7ILYjYQ+6CSs8yNJSK9jHJKN6cw==} - '@boxyhq/saml-jackson@1.37.1': - resolution: {integrity: sha512-Bceg8NrhatEvZLIrDj2ALsSMW6/pvURASmdqK/HdpKqOfGKrId8imWrbnWcj0laEqAltc/WjYsI4NeLwpuOytw==} + '@boxyhq/saml-jackson@1.44.0': + resolution: {integrity: sha512-BQo045nyKa0RPwkXlUVi1xL8ObjCAhDIl9XvxcZVfLFWC7i/Wr70Mg0azfcdlRY44EjE6TEwNMybiE2FloUKRA==} engines: {node: '>=16', npm: '>=8'} - '@boxyhq/saml20@1.7.1': - resolution: {integrity: sha512-WOKJKcXhnI69d7qiDgCRTfXC6BFDWUq9qm9av23JAYXj+RN0MYaNVVZo4FHyJ4f0b/SaO97vLub/fjwC56Cb/w==} + '@boxyhq/saml20@1.10.1': + resolution: {integrity: sha512-LViW69mFUTG1aNCrMz8qOCcwJgg8a/svDSEKt+60cuCTYy7PbCeIbmZhe7SMPBOUIaBBjMid67lYqnKz8oj2Vg==} '@calcom/embed-core@1.5.1': resolution: {integrity: sha512-wykzh1GKj5xhGxDJeCRJ7OulAgn9GVMYD/mmOBbvn06c3m9Lqoqn09E5kJ+DY+aokUncQPcstNsdiHsURjMuVw==} @@ -2375,438 +2404,150 @@ packages: '@emnapi/wasi-threads@1.0.1': resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.24.2': - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.2': resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.24.2': - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.2': resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.24.2': - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.2': resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.2': resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.2': resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.2': resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.24.2': - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.2': resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.24.2': - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.2': resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.2': resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.24.2': - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.2': resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.24.2': - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.2': resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.2': resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.24.2': - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.2': resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.2': resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.2': resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.2': resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.2': resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.25.2': resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.2': resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.25.2': resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.2': resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.2': resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.2': resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.2': resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.24.2': - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.2': resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} engines: {node: '>=18'} @@ -2937,14 +2678,10 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@googleapis/admin@23.0.0': - resolution: {integrity: sha512-6UFpKC6A7gPEysbIhJVNPeFc91jt9FGdZLndKLAQjqr6j1k4zOBTf5OMDmmket+h74spuiz18WO/lCTJvSGYNQ==} + '@googleapis/admin@23.3.0': + resolution: {integrity: sha512-89hD2B1HKc3WjCyxod3Vu4nGCmPZc1gmfidBj+soKlQfZYm7Tml1F/GQkar8tOUL+em2XCYQ1X1xxhrLd/OgwQ==} engines: {node: '>=12.0.0'} - '@grpc/grpc-js@1.11.3': - resolution: {integrity: sha512-i9UraDzFHMR+Iz/MhFLljT+fCpgxZ3O6CxwGJ8YuNYHJItIHUzKJpW2LvoFZNnGPwqc9iWy9RAucxV0JoR9aUQ==} - engines: {node: '>=12.10.0'} - '@grpc/grpc-js@1.12.6': resolution: {integrity: sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==} engines: {node: '>=12.10.0'} @@ -3404,6 +3141,10 @@ packages: resolution: {integrity: sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==} engines: {node: '>=14'} + '@opentelemetry/api-logs@0.57.1': + resolution: {integrity: sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==} + engines: {node: '>=14'} + '@opentelemetry/api-logs@0.57.2': resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} engines: {node: '>=14'} @@ -3422,12 +3163,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@1.26.0': - resolution: {integrity: sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@1.29.0': resolution: {integrity: sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA==} engines: {node: '>=14'} @@ -3446,14 +3181,14 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-metrics-otlp-grpc@0.53.0': - resolution: {integrity: sha512-2wjAccaG4yBxjfPqDeeXEYymwo1OYybUmBxUutDPeu0ColVkXyHIOxKSdHdn6vAn/v20m4w9E6SrSl4jtuZdiA==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.1': + resolution: {integrity: sha512-8B7k5q4AUldbfvubcHApg1XQaio/cO/VUWsM5PSaRP2fsjGNwbn2ih04J3gLD+AmgslvyuDcA2SZiDXEKwAxtQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.53.0': - resolution: {integrity: sha512-nvZtOk23pZOrTW10Za2WPd9pk4tWDvL6ALlHRFfInpcTjtOgCrv+fQDxpzosa5PeXvYeFFUO5aYCTnwiCX4Dzg==} + '@opentelemetry/exporter-metrics-otlp-http@0.57.1': + resolution: {integrity: sha512-jpKYVZY7fdwTdy+eAy/Mp9DZMaQpj7caMzlo3QqQDSJx5FZEY6zWzgcKvDvF6h+gdHE7LgUjaPOvJVUs354jJg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -3644,20 +3379,20 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.53.0': - resolution: {integrity: sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==} + '@opentelemetry/otlp-exporter-base@0.57.1': + resolution: {integrity: sha512-GNBJAEYfeiYJQ3O2dvXgiNZ/qjWrBxSb1L1s7iV/jKBRGMN3Nv+miTk2SLeEobF5E5ZK4rVcHKlBZ71bPVIv/g==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': ^1.0.0 + '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.53.0': - resolution: {integrity: sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==} + '@opentelemetry/otlp-grpc-exporter-base@0.57.1': + resolution: {integrity: sha512-wWflmkDhH/3wf6yEqPmzmqA6r+A8+LQABfIVZC0jDGtWVJj6eCWcGHU41UxupMbbsgjZRLYtWDilaCHOjmR7gg==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': ^1.0.0 + '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.53.0': - resolution: {integrity: sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==} + '@opentelemetry/otlp-transformer@0.57.1': + resolution: {integrity: sha512-EX67y+ukNNfFrOLyjYGw8AMy0JPIlEX1dW60SGUNZWW2hSQyyolX7EqFuHP5LtXLjJHNfzx5SMBVQ3owaQCNDw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -3666,12 +3401,6 @@ packages: resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} engines: {node: '>=14'} - '@opentelemetry/resources@1.26.0': - resolution: {integrity: sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/resources@1.29.0': resolution: {integrity: sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ==} engines: {node: '>=14'} @@ -3690,23 +3419,17 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.53.0': - resolution: {integrity: sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-logs@0.56.0': resolution: {integrity: sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@1.26.0': - resolution: {integrity: sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==} + '@opentelemetry/sdk-logs@0.57.1': + resolution: {integrity: sha512-jGdObb/BGWu6Peo3cL3skx/Rl1Ak/wDDO3vpPrrThGbqE7isvkCsX6uE+OAt8Ayjm9YC8UGkohWbLR09JmM0FA==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/api': '>=1.4.0 <1.10.0' '@opentelemetry/sdk-metrics@1.30.1': resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} @@ -3714,12 +3437,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@1.26.0': - resolution: {integrity: sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-base@1.30.1': resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} engines: {node: '>=14'} @@ -3734,6 +3451,10 @@ packages: resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.29.0': + resolution: {integrity: sha512-KZ1JsXcP2pqunfsJBNk+py6AJ5R6ZJ3yvM5Lhhf93rHPHvdDzgfMYPS4F7GNO3j/MVDCtfbttrkcpu7sl0Wu/Q==} + engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.30.0': resolution: {integrity: sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==} engines: {node: '>=14'} @@ -6135,29 +5856,11 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - '@vitest/coverage-v8@2.1.8': - resolution: {integrity: sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==} + '@vitest/coverage-v8@3.1.1': + resolution: {integrity: sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==} peerDependencies: - '@vitest/browser': 2.1.8 - vitest: 2.1.8 - peerDependenciesMeta: - '@vitest/browser': - optional: true - - '@vitest/coverage-v8@3.0.4': - resolution: {integrity: sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==} - peerDependencies: - '@vitest/browser': 3.0.4 - vitest: 3.0.4 - peerDependenciesMeta: - '@vitest/browser': - optional: true - - '@vitest/coverage-v8@3.0.7': - resolution: {integrity: sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==} - peerDependencies: - '@vitest/browser': 3.0.7 - vitest: 3.0.7 + '@vitest/browser': 3.1.1 + vitest: 3.1.1 peerDependenciesMeta: '@vitest/browser': optional: true @@ -6165,65 +5868,9 @@ packages: '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - - '@vitest/expect@3.0.5': - resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} - - '@vitest/expect@3.0.6': - resolution: {integrity: sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg==} - - '@vitest/expect@3.0.7': - resolution: {integrity: sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==} - '@vitest/expect@3.1.1': resolution: {integrity: sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==} - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/mocker@3.0.5': - resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/mocker@3.0.6': - resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/mocker@3.0.7': - resolution: {integrity: sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/mocker@3.1.1': resolution: {integrity: sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==} peerDependencies: @@ -6241,66 +5888,18 @@ packages: '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.0.5': - resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} - - '@vitest/pretty-format@3.0.6': - resolution: {integrity: sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==} - - '@vitest/pretty-format@3.0.7': - resolution: {integrity: sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==} - - '@vitest/pretty-format@3.0.9': - resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} - '@vitest/pretty-format@3.1.1': resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} - '@vitest/runner@2.1.9': - resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - - '@vitest/runner@3.0.5': - resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} - - '@vitest/runner@3.0.6': - resolution: {integrity: sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==} - - '@vitest/runner@3.0.7': - resolution: {integrity: sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==} - '@vitest/runner@3.1.1': resolution: {integrity: sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - - '@vitest/snapshot@3.0.5': - resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} - - '@vitest/snapshot@3.0.6': - resolution: {integrity: sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==} - - '@vitest/snapshot@3.0.7': - resolution: {integrity: sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==} - '@vitest/snapshot@3.1.1': resolution: {integrity: sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==} '@vitest/spy@2.0.5': resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - - '@vitest/spy@3.0.5': - resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} - - '@vitest/spy@3.0.6': - resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==} - - '@vitest/spy@3.0.7': - resolution: {integrity: sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==} - '@vitest/spy@3.1.1': resolution: {integrity: sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==} @@ -6310,15 +5909,6 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@3.0.5': - resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} - - '@vitest/utils@3.0.6': - resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==} - - '@vitest/utils@3.0.7': - resolution: {integrity: sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==} - '@vitest/utils@3.1.1': resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==} @@ -6340,14 +5930,6 @@ packages: '@vue/compiler-vue2@2.7.16': resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} - '@vue/language-core@2.1.6': - resolution: {integrity: sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@vue/language-core@2.2.0': resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} peerDependencies: @@ -6420,8 +6002,8 @@ packages: resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} - '@xmldom/xmldom@0.9.7': - resolution: {integrity: sha512-syvR8iIJjpTZ/stv7l89UAViwGFh6lbheeOaqSxkYx9YNmIVvPTRH+CT/fpykFtUx5N+8eSMDRvggF9J8GEPzQ==} + '@xmldom/xmldom@0.9.8': + resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} engines: {node: '>=14.6'} '@xobotyi/scrollbar-width@1.9.5': @@ -6600,6 +6182,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansis@3.17.0: + resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -6774,9 +6360,6 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.7.9: - resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} - axios@1.8.4: resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} @@ -7257,11 +6840,6 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -7284,9 +6862,6 @@ packages: cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -7412,9 +6987,6 @@ packages: resolution: {integrity: sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==} engines: {node: '>= 0.8.0'} - computeds@0.0.1: - resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -8005,16 +7577,6 @@ packages: peerDependencies: esbuild: '>=0.12 <1' - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.25.2: resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} engines: {node: '>=18'} @@ -8962,9 +8524,6 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} @@ -9547,8 +9106,8 @@ packages: jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} - jose@5.9.6: - resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + jose@6.0.10: + resolution: {integrity: sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -9924,10 +9483,6 @@ packages: resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} engines: {node: '>=8.9.0'} - local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} - local-pkg@1.1.1: resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} engines: {node: '>=14'} @@ -10531,8 +10086,8 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} - mixpanel@0.18.0: - resolution: {integrity: sha512-VyUoiLB/S/7abYYHGD5x0LijeuJCUabG8Hb+FvYU3Y99xHf1Qh+s4/pH9lt50fRitAHncWbU1FE01EknUfVVjQ==} + mixpanel@0.18.1: + resolution: {integrity: sha512-YD1xfn6WP6ZLQ6Pmgh0KgdXhueJEsrodThMTsHzHMH0VbWa9ck8s+ynDtM83OSgt+yQ61W/SQNrH8Y4wIwocGg==} engines: {node: '>=10.0'} mkdirp-classic@0.5.3: @@ -10547,11 +10102,6 @@ packages: engines: {node: '>=10'} hasBin: true - mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} - engines: {node: '>=10'} - hasBin: true - mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -10564,8 +10114,8 @@ packages: mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - mongodb@6.13.0: - resolution: {integrity: sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==} + mongodb@6.15.0: + resolution: {integrity: sha512-ifBhQ0rRzHDzqp9jAQP6OwHSH7dbYIQjD3SbJs9YYk9AikKEettW/9s/tbSFDTpXcRbF+u1aLrhHxDFaYtZpFQ==} engines: {node: '>=16.20.1'} peerDependencies: '@aws-sdk/credential-providers': ^3.188.0 @@ -10630,8 +10180,8 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - mysql2@3.12.0: - resolution: {integrity: sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==} + mysql2@3.14.0: + resolution: {integrity: sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==} engines: {node: '>= 8.0'} mz@2.7.0: @@ -11017,8 +10567,8 @@ packages: openid-client@5.7.1: resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} - openid-client@6.1.7: - resolution: {integrity: sha512-JfY/KvQgOutmG2P+oVNKInE7zIh+im1MQOaO7g5CtNnTWMociA563WweiEMKfR9ry9XG3K2HGvj9wEqhCQkPMg==} + openid-client@6.3.4: + resolution: {integrity: sha512-CGZGk9Y6Bv9R4bXlrzVoxzD1n4h8iP914UhjVyRSftqzqO4CWaRqKpOmW253Jmpv4EWkz7/Gut/90iiWW8t0ow==} opentelemetry@0.1.0: resolution: {integrity: sha512-8w5sK99P1ZG25WIvHvIa0mSyQ96hl08VQ+1SUrnSg68O85P9ZqjtRwipAftaJW+QvoxxrK7S+Zu9KOqOA+lNhg==} @@ -11144,15 +10694,6 @@ packages: resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} engines: {node: '>=10'} - parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - - parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} @@ -11214,9 +10755,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -11256,8 +10794,8 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.13.1: - resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==} + pg@8.14.1: + resolution: {integrity: sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -12582,6 +12120,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sql-highlight@6.0.0: + resolution: {integrity: sha512-+fLpbAbWkQ+d0JEchJT/NrRRXbYRNbG15gFpANx73EwxQB1PRjj+k/OI0GTU0J63g8ikGkJECQp9z8XEJZvPRw==} + engines: {node: '>=14'} + sqlite3@5.1.7: resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} @@ -13285,24 +12827,25 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typeorm@0.3.20: - resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} + typeorm@0.3.21: + resolution: {integrity: sha512-lh4rUWl1liZGjyPTWpwcK8RNI5x4ekln+/JJOox1wCd7xbucYDOXWD+1cSzTN3L0wbTGxxOtloM5JlxbOxEufA==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: '@google-cloud/spanner': ^5.18.0 '@sap/hana-client': ^2.12.25 - better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 hdb-pool: ^0.1.6 ioredis: ^5.0.4 mongodb: ^5.8.0 - mssql: ^9.1.1 || ^10.0.1 + mssql: ^9.1.1 || ^10.0.1 || ^11.0.1 mysql2: ^2.2.5 || ^3.0.1 oracledb: ^6.3.0 pg: ^8.5.1 pg-native: ^3.0.0 pg-query-stream: ^4.0.0 redis: ^3.1.1 || ^4.0.0 + reflect-metadata: ^0.1.14 || ^0.2.0 sql.js: ^1.4.0 sqlite3: ^5.0.3 ts-node: ^10.7.0 @@ -13567,41 +13110,11 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - vite-node@3.0.5: - resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite-node@3.0.6: - resolution: {integrity: sha512-s51RzrTkXKJrhNbUzQRsarjmAae7VmMPAsRT7lppVpIg6mK3zGthP9Hgz0YQQKuNcF+Ii7DfYk3Fxz40jRmePw==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite-node@3.0.7: - resolution: {integrity: sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - vite-node@3.1.1: resolution: {integrity: sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-plugin-dts@4.3.0: - resolution: {integrity: sha512-LkBJh9IbLwL6/rxh0C1/bOurDrIEmRE7joC+jFdOEEciAFPbpEKOLSAr5nNh5R7CJ45cMbksTrFfy52szzC5eA==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - typescript: '*' - vite: '*' - peerDependenciesMeta: - vite: - optional: true - vite-plugin-dts@4.5.3: resolution: {integrity: sha512-P64VnD00dR+e8S26ESoFELqc17+w7pKkwlBpgXteOljFyT0zDwD8hH4zXp49M/kciy//7ZbVXIwQCekBJjfWzA==} peerDependencies: @@ -13624,117 +13137,6 @@ packages: vite: optional: true - vite@5.4.15: - resolution: {integrity: sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vite@6.0.12: - resolution: {integrity: sha512-gzLogvGSgX2xyAt0J5qhJ7SmdO5aLdShABkU8Ev7dIl8AcrlFSLcj9GHReSq9pGJF/q5C4CZKdtDlkC6DyvQ3w==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vite@6.2.3: - resolution: {integrity: sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@6.2.4: resolution: {integrity: sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -13775,120 +13177,11 @@ packages: yaml: optional: true - vitest-mock-extended@2.0.2: - resolution: {integrity: sha512-n3MBqVITKyclZ0n0y66hkT4UiiEYFQn9tteAnIxT0MPz1Z8nFcPUG3Cf0cZOyoPOj/cq6Ab1XFw2lM/qM5EDWQ==} + vitest-mock-extended@3.0.1: + resolution: {integrity: sha512-VI7CRRvIi+MbAsqdGTxp3K+eiY7BR1zrVflZ5DBrFUXPjRZRgxXajlYdNyIu3v1bb5ZfdLANXwZ9i/RfVMfS6A==} peerDependencies: typescript: 3.x || 4.x || 5.x - vitest: '>=2.0.0' - - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - vitest@3.0.5: - resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.5 - '@vitest/ui': 3.0.5 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - vitest@3.0.6: - resolution: {integrity: sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.6 - '@vitest/ui': 3.0.6 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - vitest@3.0.7: - resolution: {integrity: sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.7 - '@vitest/ui': 3.0.7 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true + vitest: '>=3.0.0' vitest@3.1.1: resolution: {integrity: sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==} @@ -14141,8 +13434,8 @@ packages: engines: {node: '>=0.8'} hasBin: true - xml-crypto@6.0.0: - resolution: {integrity: sha512-L3RgnkaDrHaYcCnoENv4Idzt1ZRj5U1z1BDH98QdDTQfssScx8adgxhd9qwyYo+E3fXbQZjEQH7aiXHLVgxGvw==} + xml-crypto@6.0.1: + resolution: {integrity: sha512-v05aU7NS03z4jlZ0iZGRFeZsuKO1UfEbbYiaeRMiATBFs6Jq9+wqKquEMTn4UTrYZ9iGD8yz3KT4L9o2iF682w==} engines: {node: '>=16'} xml-encryption@3.1.0: @@ -14209,10 +13502,6 @@ packages: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -14221,10 +13510,6 @@ packages: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -14408,21 +13693,21 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-cognito-identity@3.741.0(aws-crt@1.25.3)': + '@aws-sdk/client-cognito-identity@3.774.0(aws-crt@1.25.3)': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.734.0 - '@aws-sdk/credential-provider-node': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/core': 3.774.0 + '@aws-sdk/credential-provider-node': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/middleware-host-header': 3.774.0 '@aws-sdk/middleware-logger': 3.734.0 - '@aws-sdk/middleware-recursion-detection': 3.734.0 - '@aws-sdk/middleware-user-agent': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.772.0 + '@aws-sdk/middleware-user-agent': 3.774.0 '@aws-sdk/region-config-resolver': 3.734.0 '@aws-sdk/types': 3.734.0 - '@aws-sdk/util-endpoints': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 '@aws-sdk/util-user-agent-browser': 3.734.0 - '@aws-sdk/util-user-agent-node': 3.734.0(aws-crt@1.25.3) + '@aws-sdk/util-user-agent-node': 3.774.0(aws-crt@1.25.3) '@smithy/config-resolver': 4.1.0 '@smithy/core': 3.2.0 '@smithy/fetch-http-handler': 5.0.2 @@ -14452,22 +13737,22 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-dynamodb@3.741.0(aws-crt@1.25.3)': + '@aws-sdk/client-dynamodb@3.774.0(aws-crt@1.25.3)': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.734.0 - '@aws-sdk/credential-provider-node': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/middleware-endpoint-discovery': 3.734.0 - '@aws-sdk/middleware-host-header': 3.734.0 + '@aws-sdk/core': 3.774.0 + '@aws-sdk/credential-provider-node': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/middleware-endpoint-discovery': 3.774.0 + '@aws-sdk/middleware-host-header': 3.774.0 '@aws-sdk/middleware-logger': 3.734.0 - '@aws-sdk/middleware-recursion-detection': 3.734.0 - '@aws-sdk/middleware-user-agent': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.772.0 + '@aws-sdk/middleware-user-agent': 3.774.0 '@aws-sdk/region-config-resolver': 3.734.0 '@aws-sdk/types': 3.734.0 - '@aws-sdk/util-endpoints': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 '@aws-sdk/util-user-agent-browser': 3.734.0 - '@aws-sdk/util-user-agent-node': 3.734.0(aws-crt@1.25.3) + '@aws-sdk/util-user-agent-node': 3.774.0(aws-crt@1.25.3) '@smithy/config-resolver': 4.1.0 '@smithy/core': 3.2.0 '@smithy/fetch-http-handler': 5.0.2 @@ -14604,6 +13889,49 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-sso@3.774.0(aws-crt@1.25.3)': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.774.0 + '@aws-sdk/middleware-host-header': 3.774.0 + '@aws-sdk/middleware-logger': 3.734.0 + '@aws-sdk/middleware-recursion-detection': 3.772.0 + '@aws-sdk/middleware-user-agent': 3.774.0 + '@aws-sdk/region-config-resolver': 3.734.0 + '@aws-sdk/types': 3.734.0 + '@aws-sdk/util-endpoints': 3.743.0 + '@aws-sdk/util-user-agent-browser': 3.734.0 + '@aws-sdk/util-user-agent-node': 3.774.0(aws-crt@1.25.3) + '@smithy/config-resolver': 4.1.0 + '@smithy/core': 3.2.0 + '@smithy/fetch-http-handler': 5.0.2 + '@smithy/hash-node': 4.0.2 + '@smithy/invalid-dependency': 4.0.2 + '@smithy/middleware-content-length': 4.0.2 + '@smithy/middleware-endpoint': 4.1.0 + '@smithy/middleware-retry': 4.1.0 + '@smithy/middleware-serde': 4.0.3 + '@smithy/middleware-stack': 4.0.2 + '@smithy/node-config-provider': 4.0.2 + '@smithy/node-http-handler': 4.0.4 + '@smithy/protocol-http': 5.1.0 + '@smithy/smithy-client': 4.2.0 + '@smithy/types': 4.2.0 + '@smithy/url-parser': 4.0.2 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.8 + '@smithy/util-defaults-mode-node': 4.0.8 + '@smithy/util-endpoints': 3.0.2 + '@smithy/util-middleware': 4.0.2 + '@smithy/util-retry': 4.0.2 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/core@3.734.0': dependencies: '@aws-sdk/types': 3.734.0 @@ -14631,11 +13959,10 @@ snapshots: '@smithy/util-middleware': 4.0.2 fast-xml-parser: 4.4.1 tslib: 2.8.1 - optional: true - '@aws-sdk/credential-provider-cognito-identity@3.741.0(aws-crt@1.25.3)': + '@aws-sdk/credential-provider-cognito-identity@3.774.0(aws-crt@1.25.3)': dependencies: - '@aws-sdk/client-cognito-identity': 3.741.0(aws-crt@1.25.3) + '@aws-sdk/client-cognito-identity': 3.774.0(aws-crt@1.25.3) '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.2 '@smithy/types': 4.2.0 @@ -14651,6 +13978,14 @@ snapshots: '@smithy/types': 4.2.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.774.0': + dependencies: + '@aws-sdk/core': 3.774.0 + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.0.2 + '@smithy/types': 4.2.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.734.0': dependencies: '@aws-sdk/core': 3.734.0 @@ -14664,6 +13999,19 @@ snapshots: '@smithy/util-stream': 4.2.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.774.0': + dependencies: + '@aws-sdk/core': 3.774.0 + '@aws-sdk/types': 3.734.0 + '@smithy/fetch-http-handler': 5.0.2 + '@smithy/node-http-handler': 4.0.4 + '@smithy/property-provider': 4.0.2 + '@smithy/protocol-http': 5.1.0 + '@smithy/smithy-client': 4.2.0 + '@smithy/types': 4.2.0 + '@smithy/util-stream': 4.2.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.741.0(aws-crt@1.25.3)': dependencies: '@aws-sdk/core': 3.734.0 @@ -14682,6 +14030,24 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-ini@3.774.0(aws-crt@1.25.3)': + dependencies: + '@aws-sdk/core': 3.774.0 + '@aws-sdk/credential-provider-env': 3.774.0 + '@aws-sdk/credential-provider-http': 3.774.0 + '@aws-sdk/credential-provider-process': 3.774.0 + '@aws-sdk/credential-provider-sso': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-provider-web-identity': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/nested-clients': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/types': 3.734.0 + '@smithy/credential-provider-imds': 4.0.2 + '@smithy/property-provider': 4.0.2 + '@smithy/shared-ini-file-loader': 4.0.2 + '@smithy/types': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-node@3.741.0(aws-crt@1.25.3)': dependencies: '@aws-sdk/credential-provider-env': 3.734.0 @@ -14699,6 +14065,23 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-node@3.774.0(aws-crt@1.25.3)': + dependencies: + '@aws-sdk/credential-provider-env': 3.774.0 + '@aws-sdk/credential-provider-http': 3.774.0 + '@aws-sdk/credential-provider-ini': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-provider-process': 3.774.0 + '@aws-sdk/credential-provider-sso': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-provider-web-identity': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/types': 3.734.0 + '@smithy/credential-provider-imds': 4.0.2 + '@smithy/property-provider': 4.0.2 + '@smithy/shared-ini-file-loader': 4.0.2 + '@smithy/types': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-process@3.734.0': dependencies: '@aws-sdk/core': 3.734.0 @@ -14708,6 +14091,15 @@ snapshots: '@smithy/types': 4.2.0 tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.774.0': + dependencies: + '@aws-sdk/core': 3.774.0 + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.0.2 + '@smithy/shared-ini-file-loader': 4.0.2 + '@smithy/types': 4.2.0 + tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.734.0(aws-crt@1.25.3)': dependencies: '@aws-sdk/client-sso': 3.734.0(aws-crt@1.25.3) @@ -14721,6 +14113,19 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-sso@3.774.0(aws-crt@1.25.3)': + dependencies: + '@aws-sdk/client-sso': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/core': 3.774.0 + '@aws-sdk/token-providers': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.0.2 + '@smithy/shared-ini-file-loader': 4.0.2 + '@smithy/types': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-web-identity@3.734.0(aws-crt@1.25.3)': dependencies: '@aws-sdk/core': 3.734.0 @@ -14742,21 +14147,20 @@ snapshots: tslib: 2.8.1 transitivePeerDependencies: - aws-crt - optional: true - '@aws-sdk/credential-providers@3.741.0(aws-crt@1.25.3)': + '@aws-sdk/credential-providers@3.774.0(aws-crt@1.25.3)': dependencies: - '@aws-sdk/client-cognito-identity': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/core': 3.734.0 - '@aws-sdk/credential-provider-cognito-identity': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/credential-provider-env': 3.734.0 - '@aws-sdk/credential-provider-http': 3.734.0 - '@aws-sdk/credential-provider-ini': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/credential-provider-node': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/credential-provider-process': 3.734.0 - '@aws-sdk/credential-provider-sso': 3.734.0(aws-crt@1.25.3) - '@aws-sdk/credential-provider-web-identity': 3.734.0(aws-crt@1.25.3) - '@aws-sdk/nested-clients': 3.734.0(aws-crt@1.25.3) + '@aws-sdk/client-cognito-identity': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/core': 3.774.0 + '@aws-sdk/credential-provider-cognito-identity': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-provider-env': 3.774.0 + '@aws-sdk/credential-provider-http': 3.774.0 + '@aws-sdk/credential-provider-ini': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-provider-node': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-provider-process': 3.774.0 + '@aws-sdk/credential-provider-sso': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-provider-web-identity': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/nested-clients': 3.774.0(aws-crt@1.25.3) '@aws-sdk/types': 3.734.0 '@smithy/core': 3.2.0 '@smithy/credential-provider-imds': 4.0.2 @@ -14781,7 +14185,7 @@ snapshots: '@smithy/util-config-provider': 4.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-endpoint-discovery@3.734.0': + '@aws-sdk/middleware-endpoint-discovery@3.774.0': dependencies: '@aws-sdk/endpoint-cache': 3.723.0 '@aws-sdk/types': 3.734.0 @@ -14826,7 +14230,6 @@ snapshots: '@smithy/protocol-http': 5.1.0 '@smithy/types': 4.2.0 tslib: 2.8.1 - optional: true '@aws-sdk/middleware-location-constraint@3.734.0': dependencies: @@ -14853,7 +14256,6 @@ snapshots: '@smithy/protocol-http': 5.1.0 '@smithy/types': 4.2.0 tslib: 2.8.1 - optional: true '@aws-sdk/middleware-sdk-s3@3.740.0': dependencies: @@ -14897,7 +14299,6 @@ snapshots: '@smithy/protocol-http': 5.1.0 '@smithy/types': 4.2.0 tslib: 2.8.1 - optional: true '@aws-sdk/nested-clients@3.734.0(aws-crt@1.25.3)': dependencies: @@ -14984,7 +14385,6 @@ snapshots: tslib: 2.8.1 transitivePeerDependencies: - aws-crt - optional: true '@aws-sdk/region-config-resolver@3.734.0': dependencies: @@ -15040,6 +14440,17 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/token-providers@3.774.0(aws-crt@1.25.3)': + dependencies: + '@aws-sdk/nested-clients': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/types': 3.734.0 + '@smithy/property-provider': 4.0.2 + '@smithy/shared-ini-file-loader': 4.0.2 + '@smithy/types': 4.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/types@3.734.0': dependencies: '@smithy/types': 4.2.0 @@ -15049,9 +14460,9 @@ snapshots: dependencies: tslib: 2.8.1 - '@aws-sdk/util-dynamodb@3.741.0(@aws-sdk/client-dynamodb@3.741.0(aws-crt@1.25.3))': + '@aws-sdk/util-dynamodb@3.774.0(@aws-sdk/client-dynamodb@3.774.0(aws-crt@1.25.3))': dependencies: - '@aws-sdk/client-dynamodb': 3.741.0(aws-crt@1.25.3) + '@aws-sdk/client-dynamodb': 3.774.0(aws-crt@1.25.3) tslib: 2.8.1 '@aws-sdk/util-endpoints@3.734.0': @@ -15067,7 +14478,6 @@ snapshots: '@smithy/types': 4.2.0 '@smithy/util-endpoints': 3.0.2 tslib: 2.8.1 - optional: true '@aws-sdk/util-format-url@3.734.0': dependencies: @@ -15106,7 +14516,6 @@ snapshots: tslib: 2.8.1 optionalDependencies: aws-crt: 1.25.3 - optional: true '@aws-sdk/util-utf8-browser@3.259.0': dependencies: @@ -16222,48 +15631,46 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@bcoe/v8-coverage@0.2.3': {} - '@bcoe/v8-coverage@1.0.2': {} '@boxyhq/error-code-mnemonic@0.1.1': {} - '@boxyhq/metrics@0.2.9': + '@boxyhq/metrics@0.2.10': dependencies: - '@grpc/grpc-js': 1.11.3 + '@grpc/grpc-js': 1.12.6 '@opentelemetry/api': 1.9.0 - '@opentelemetry/exporter-metrics-otlp-grpc': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.27.0 + '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.29.0 - '@boxyhq/saml-jackson@1.37.1(aws-crt@1.25.3)(socks@2.8.4)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2))': + '@boxyhq/saml-jackson@1.44.0(aws-crt@1.25.3)(socks@2.8.4)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2))': dependencies: - '@aws-sdk/client-dynamodb': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/credential-providers': 3.741.0(aws-crt@1.25.3) - '@aws-sdk/util-dynamodb': 3.741.0(@aws-sdk/client-dynamodb@3.741.0(aws-crt@1.25.3)) + '@aws-sdk/client-dynamodb': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/credential-providers': 3.774.0(aws-crt@1.25.3) + '@aws-sdk/util-dynamodb': 3.774.0(@aws-sdk/client-dynamodb@3.774.0(aws-crt@1.25.3)) '@boxyhq/error-code-mnemonic': 0.1.1 - '@boxyhq/metrics': 0.2.9 - '@boxyhq/saml20': 1.7.1 - '@googleapis/admin': 23.0.0(encoding@0.1.13) + '@boxyhq/metrics': 0.2.10 + '@boxyhq/saml20': 1.10.1 + '@googleapis/admin': 23.3.0(encoding@0.1.13) '@libsql/sqlite3': 0.3.1(encoding@0.1.13) - axios: 1.7.9 + axios: 1.8.4 encoding: 0.1.13 - jose: 5.9.6 + jose: 6.0.10 lodash: 4.17.21 - mixpanel: 0.18.0 - mongodb: 6.13.0(@aws-sdk/credential-providers@3.741.0(aws-crt@1.25.3))(socks@2.8.4) + mixpanel: 0.18.1 + mongodb: 6.15.0(@aws-sdk/credential-providers@3.774.0(aws-crt@1.25.3))(socks@2.8.4) mssql: 11.0.1 - mysql2: 3.12.0 + mysql2: 3.14.0 node-forge: 1.3.1 - openid-client: 6.1.7 - pg: 8.13.1 + openid-client: 6.3.4 + pg: 8.14.1 redis: 4.7.0 reflect-metadata: 0.2.2 ripemd160: 2.0.2 sqlite3: 5.1.7 - typeorm: 0.3.20(mongodb@6.13.0(@aws-sdk/credential-providers@3.741.0(aws-crt@1.25.3))(socks@2.8.4))(mssql@11.0.1)(mysql2@3.12.0)(pg@8.13.1)(redis@4.7.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)) + typeorm: 0.3.21(mongodb@6.15.0(@aws-sdk/credential-providers@3.774.0(aws-crt@1.25.3))(socks@2.8.4))(mssql@11.0.1)(mysql2@3.14.0)(pg@8.14.1)(redis@4.7.0)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)) transitivePeerDependencies: - '@google-cloud/spanner' - '@mongodb-js/zstd' @@ -16289,10 +15696,10 @@ snapshots: - typeorm-aurora-data-api-driver - utf-8-validate - '@boxyhq/saml20@1.7.1': + '@boxyhq/saml20@1.10.1': dependencies: - '@xmldom/xmldom': 0.9.7 - xml-crypto: 6.0.0 + '@xmldom/xmldom': 0.9.8 + xml-crypto: 6.0.1 xml-encryption: 3.1.0 xml2js: 0.6.2 xmlbuilder: 15.1.1 @@ -16532,222 +15939,78 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/aix-ppc64@0.24.2': - optional: true - '@esbuild/aix-ppc64@0.25.2': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.24.2': - optional: true - '@esbuild/android-arm64@0.25.2': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-arm@0.24.2': - optional: true - '@esbuild/android-arm@0.25.2': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/android-x64@0.24.2': - optional: true - '@esbuild/android-x64@0.25.2': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.24.2': - optional: true - '@esbuild/darwin-arm64@0.25.2': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.24.2': - optional: true - '@esbuild/darwin-x64@0.25.2': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.24.2': - optional: true - '@esbuild/freebsd-arm64@0.25.2': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.24.2': - optional: true - '@esbuild/freebsd-x64@0.25.2': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.24.2': - optional: true - '@esbuild/linux-arm64@0.25.2': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-arm@0.24.2': - optional: true - '@esbuild/linux-arm@0.25.2': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.24.2': - optional: true - '@esbuild/linux-ia32@0.25.2': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.24.2': - optional: true - '@esbuild/linux-loong64@0.25.2': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.24.2': - optional: true - '@esbuild/linux-mips64el@0.25.2': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.24.2': - optional: true - '@esbuild/linux-ppc64@0.25.2': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.24.2': - optional: true - '@esbuild/linux-riscv64@0.25.2': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.24.2': - optional: true - '@esbuild/linux-s390x@0.25.2': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/linux-x64@0.24.2': - optional: true - '@esbuild/linux-x64@0.25.2': optional: true - '@esbuild/netbsd-arm64@0.24.2': - optional: true - '@esbuild/netbsd-arm64@0.25.2': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.24.2': - optional: true - '@esbuild/netbsd-x64@0.25.2': optional: true - '@esbuild/openbsd-arm64@0.24.2': - optional: true - '@esbuild/openbsd-arm64@0.25.2': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.24.2': - optional: true - '@esbuild/openbsd-x64@0.25.2': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.24.2': - optional: true - '@esbuild/sunos-x64@0.25.2': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.24.2': - optional: true - '@esbuild/win32-arm64@0.25.2': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.24.2': - optional: true - '@esbuild/win32-ia32@0.25.2': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - - '@esbuild/win32-x64@0.24.2': - optional: true - '@esbuild/win32-x64@0.25.2': optional: true @@ -17105,18 +16368,13 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@googleapis/admin@23.0.0(encoding@0.1.13)': + '@googleapis/admin@23.3.0(encoding@0.1.13)': dependencies: googleapis-common: 7.2.0(encoding@0.1.13) transitivePeerDependencies: - encoding - supports-color - '@grpc/grpc-js@1.11.3': - dependencies: - '@grpc/proto-loader': 0.7.13 - '@js-sdsl/ordered-map': 4.4.2 - '@grpc/grpc-js@1.12.6': dependencies: '@grpc/proto-loader': 0.7.13 @@ -17713,6 +16971,10 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.57.1': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.57.2': dependencies: '@opentelemetry/api': 1.9.0 @@ -17725,11 +16987,6 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.27.0 - '@opentelemetry/core@1.29.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17745,26 +17002,26 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.30.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.53.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.1(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.12.6 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.53.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.57.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0)': dependencies: @@ -18040,39 +17297,33 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.53.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.57.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.53.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.57.1(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.12.6 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.53.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.57.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.53.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) protobufjs: 7.4.0 '@opentelemetry/redis-common@0.36.2': {} - '@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.27.0 - '@opentelemetry/resources@1.29.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -18091,13 +17342,6 @@ snapshots: '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.30.0 - '@opentelemetry/sdk-logs@0.53.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.53.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs@0.56.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -18105,11 +17349,12 @@ snapshots: '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.29.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics@1.26.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.57.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0)': dependencies: @@ -18117,13 +17362,6 @@ snapshots: '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.27.0 - '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -18135,6 +17373,8 @@ snapshots: '@opentelemetry/semantic-conventions@1.28.0': {} + '@opentelemetry/semantic-conventions@1.29.0': {} + '@opentelemetry/semantic-conventions@1.30.0': {} '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': @@ -18180,13 +17420,13 @@ snapshots: dependencies: playwright: 1.51.1 - '@preact/preset-vite@2.9.3(@babel/core@7.26.0)(preact@10.25.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': + '@preact/preset-vite@2.9.3(@babel/core@7.26.0)(preact@10.25.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-development': 7.25.9(@babel/core@7.26.0) - '@prefresh/vite': 2.4.7(preact@10.25.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + '@prefresh/vite': 2.4.7(preact@10.25.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) '@rollup/pluginutils': 4.2.1 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.26.0) debug: 4.4.0 @@ -18195,7 +17435,7 @@ snapshots: node-html-parser: 6.1.13 source-map: 0.7.4 stack-trace: 1.0.0-pre2 - vite: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - preact - supports-color @@ -18208,7 +17448,7 @@ snapshots: '@prefresh/utils@1.2.0': {} - '@prefresh/vite@2.4.7(preact@10.25.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': + '@prefresh/vite@2.4.7(preact@10.25.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@babel/core': 7.26.0 '@prefresh/babel-plugin': 0.5.1 @@ -18216,7 +17456,7 @@ snapshots: '@prefresh/utils': 1.2.0 '@rollup/pluginutils': 4.2.1 preact: 10.25.2 - vite: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -21312,25 +20552,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.8(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.4.0 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.8.1 - test-exclude: 7.0.1 - tinyrainbow: 1.2.0 - vitest: 3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - transitivePeerDependencies: - - supports-color - - '@vitest/coverage-v8@3.0.4(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': + '@vitest/coverage-v8@3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -21344,11 +20566,11 @@ snapshots: std-env: 3.8.1 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.7(vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': + '@vitest/coverage-v8@3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -21362,7 +20584,25 @@ snapshots: std-env: 3.8.1 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.6(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + debug: 4.4.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.17 + magicast: 0.3.5 + std-env: 3.8.1 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -21373,34 +20613,6 @@ snapshots: chai: 5.2.0 tinyrainbow: 1.2.0 - '@vitest/expect@2.1.9': - dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.2.0 - tinyrainbow: 1.2.0 - - '@vitest/expect@3.0.5': - dependencies: - '@vitest/spy': 3.0.5 - '@vitest/utils': 3.0.5 - chai: 5.2.0 - tinyrainbow: 2.0.0 - - '@vitest/expect@3.0.6': - dependencies: - '@vitest/spy': 3.0.6 - '@vitest/utils': 3.0.6 - chai: 5.2.0 - tinyrainbow: 2.0.0 - - '@vitest/expect@3.0.7': - dependencies: - '@vitest/spy': 3.0.7 - '@vitest/utils': 3.0.7 - chai: 5.2.0 - tinyrainbow: 2.0.0 - '@vitest/expect@3.1.1': dependencies: '@vitest/spy': 3.1.1 @@ -21408,38 +20620,22 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@2.1.9(vite@5.4.15(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0))': + '@vitest/mocker@3.1.1(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: - '@vitest/spy': 2.1.9 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 5.4.15(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0) - - '@vitest/mocker@3.0.5(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': - dependencies: - '@vitest/spy': 3.0.5 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - - '@vitest/mocker@3.0.6(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': - dependencies: - '@vitest/spy': 3.0.6 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - - '@vitest/mocker@3.0.7(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': - dependencies: - '@vitest/spy': 3.0.7 + '@vitest/spy': 3.1.1 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + '@vitest/mocker@3.1.1(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0))': + dependencies: + '@vitest/spy': 3.1.1 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + '@vitest/mocker@3.1.1(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.1.1 @@ -21456,75 +20652,15 @@ snapshots: dependencies: tinyrainbow: 1.2.0 - '@vitest/pretty-format@3.0.5': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/pretty-format@3.0.6': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/pretty-format@3.0.7': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/pretty-format@3.0.9': - dependencies: - tinyrainbow: 2.0.0 - '@vitest/pretty-format@3.1.1': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@2.1.9': - dependencies: - '@vitest/utils': 2.1.9 - pathe: 1.1.2 - - '@vitest/runner@3.0.5': - dependencies: - '@vitest/utils': 3.0.5 - pathe: 2.0.3 - - '@vitest/runner@3.0.6': - dependencies: - '@vitest/utils': 3.0.6 - pathe: 2.0.3 - - '@vitest/runner@3.0.7': - dependencies: - '@vitest/utils': 3.0.7 - pathe: 2.0.3 - '@vitest/runner@3.1.1': dependencies: '@vitest/utils': 3.1.1 pathe: 2.0.3 - '@vitest/snapshot@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.17 - pathe: 1.1.2 - - '@vitest/snapshot@3.0.5': - dependencies: - '@vitest/pretty-format': 3.0.5 - magic-string: 0.30.17 - pathe: 2.0.3 - - '@vitest/snapshot@3.0.6': - dependencies: - '@vitest/pretty-format': 3.0.6 - magic-string: 0.30.17 - pathe: 2.0.3 - - '@vitest/snapshot@3.0.7': - dependencies: - '@vitest/pretty-format': 3.0.7 - magic-string: 0.30.17 - pathe: 2.0.3 - '@vitest/snapshot@3.1.1': dependencies: '@vitest/pretty-format': 3.1.1 @@ -21535,22 +20671,6 @@ snapshots: dependencies: tinyspy: 3.0.2 - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 - - '@vitest/spy@3.0.5': - dependencies: - tinyspy: 3.0.2 - - '@vitest/spy@3.0.6': - dependencies: - tinyspy: 3.0.2 - - '@vitest/spy@3.0.7': - dependencies: - tinyspy: 3.0.2 - '@vitest/spy@3.1.1': dependencies: tinyspy: 3.0.2 @@ -21568,24 +20688,6 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - '@vitest/utils@3.0.5': - dependencies: - '@vitest/pretty-format': 3.0.5 - loupe: 3.1.3 - tinyrainbow: 2.0.0 - - '@vitest/utils@3.0.6': - dependencies: - '@vitest/pretty-format': 3.0.6 - loupe: 3.1.3 - tinyrainbow: 2.0.0 - - '@vitest/utils@3.0.7': - dependencies: - '@vitest/pretty-format': 3.0.7 - loupe: 3.1.3 - tinyrainbow: 2.0.0 - '@vitest/utils@3.1.1': dependencies: '@vitest/pretty-format': 3.1.1 @@ -21622,19 +20724,6 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 - '@vue/language-core@2.1.6(typescript@5.8.2)': - dependencies: - '@volar/language-core': 2.4.12 - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.13 - computeds: 0.0.1 - minimatch: 9.0.5 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - optionalDependencies: - typescript: 5.8.2 - '@vue/language-core@2.2.0(typescript@5.8.2)': dependencies: '@volar/language-core': 2.4.12 @@ -21734,7 +20823,7 @@ snapshots: '@xmldom/xmldom@0.8.10': {} - '@xmldom/xmldom@0.9.7': {} + '@xmldom/xmldom@0.9.8': {} '@xobotyi/scrollbar-width@1.9.5': {} @@ -21895,6 +20984,8 @@ snapshots: ansi-styles@6.2.1: {} + ansis@3.17.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -22086,14 +21177,14 @@ snapshots: atomic-sleep@1.0.0: {} - autoprefixer@10.4.20(postcss@8.4.49): + autoprefixer@10.4.20(postcss@8.5.3): dependencies: browserslist: 4.24.4 caniuse-lite: 1.0.30001707 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.4.49 + postcss: 8.5.3 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -22119,14 +21210,6 @@ snapshots: axe-core@4.10.3: {} - axios@1.7.9: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.2 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.8.4: dependencies: follow-redirects: 1.15.9 @@ -22695,15 +21778,6 @@ snapshots: dependencies: restore-cursor: 5.1.0 - cli-highlight@2.1.11: - dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - cli-spinners@2.9.2: {} cli-truncate@2.1.0: @@ -22730,12 +21804,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -22865,8 +21933,6 @@ snapshots: transitivePeerDependencies: - supports-color - computeds@0.0.1: {} - concat-map@0.0.1: {} concat-stream@2.0.0: @@ -23506,60 +22572,6 @@ snapshots: transitivePeerDependencies: - supports-color - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - - esbuild@0.24.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 - esbuild@0.25.2: optionalDependencies: '@esbuild/aix-ppc64': 0.25.2 @@ -24768,8 +23780,6 @@ snapshots: hex-rgb@4.3.0: {} - highlight.js@10.7.3: {} - hmac-drbg@1.0.1: dependencies: hash.js: 1.1.7 @@ -25387,7 +24397,7 @@ snapshots: jose@4.15.9: {} - jose@5.9.6: {} + jose@6.0.10: {} joycon@3.1.1: {} @@ -25764,11 +24774,6 @@ snapshots: emojis-list: 3.0.0 json5: 2.2.3 - local-pkg@0.5.1: - dependencies: - mlly: 1.7.4 - pkg-types: 1.3.1 - local-pkg@1.1.1: dependencies: mlly: 1.7.4 @@ -26712,7 +25717,7 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 - mixpanel@0.18.0: + mixpanel@0.18.1: dependencies: https-proxy-agent: 5.0.0 transitivePeerDependencies: @@ -26726,8 +25731,6 @@ snapshots: mkdirp@1.0.4: {} - mkdirp@2.1.6: {} - mlly@1.7.4: dependencies: acorn: 8.14.1 @@ -26746,13 +25749,13 @@ snapshots: '@types/whatwg-url': 11.0.5 whatwg-url: 14.2.0 - mongodb@6.13.0(@aws-sdk/credential-providers@3.741.0(aws-crt@1.25.3))(socks@2.8.4): + mongodb@6.15.0(@aws-sdk/credential-providers@3.774.0(aws-crt@1.25.3))(socks@2.8.4): dependencies: '@mongodb-js/saslprep': 1.2.0 bson: 6.10.3 mongodb-connection-string-url: 3.0.2 optionalDependencies: - '@aws-sdk/credential-providers': 3.741.0(aws-crt@1.25.3) + '@aws-sdk/credential-providers': 3.774.0(aws-crt@1.25.3) socks: 2.8.4 motion-dom@11.18.1: @@ -26816,7 +25819,7 @@ snapshots: mustache@4.2.0: {} - mysql2@3.12.0: + mysql2@3.14.0: dependencies: aws-ssl-profiles: 1.1.2 denque: 2.1.0 @@ -27279,9 +26282,9 @@ snapshots: object-hash: 2.2.0 oidc-token-hash: 5.1.0 - openid-client@6.1.7: + openid-client@6.3.4: dependencies: - jose: 5.9.6 + jose: 6.0.10 oauth4webapi: 3.3.1 opentelemetry@0.1.0: {} @@ -27433,14 +26436,6 @@ snapshots: dependencies: pngjs: 3.4.0 - parse5-htmlparser2-tree-adapter@6.0.1: - dependencies: - parse5: 6.0.1 - - parse5@5.1.1: {} - - parse5@6.0.1: {} - parse5@7.2.1: dependencies: entities: 4.5.0 @@ -27489,8 +26484,6 @@ snapshots: path-type@4.0.0: {} - pathe@1.1.2: {} - pathe@2.0.3: {} pathval@2.0.0: {} @@ -27514,9 +26507,9 @@ snapshots: pg-int8@1.0.1: {} - pg-pool@3.8.0(pg@8.13.1): + pg-pool@3.8.0(pg@8.14.1): dependencies: - pg: 8.13.1 + pg: 8.14.1 pg-protocol@1.8.0: {} @@ -27528,10 +26521,10 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.13.1: + pg@8.14.1: dependencies: pg-connection-string: 2.7.0 - pg-pool: 3.8.0(pg@8.13.1) + pg-pool: 3.8.0(pg@8.14.1) pg-protocol: 1.8.0 pg-types: 2.2.0 pgpass: 1.0.5 @@ -27650,24 +26643,24 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.4.49): + postcss-import@15.1.0(postcss@8.5.3): dependencies: - postcss: 8.4.49 + postcss: 8.5.3 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.4.49): + postcss-js@4.0.1(postcss@8.5.3): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.49 + postcss: 8.5.3 - postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)): + postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)): dependencies: lilconfig: 3.1.3 yaml: 2.7.0 optionalDependencies: - postcss: 8.4.49 + postcss: 8.5.3 ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.8.2) postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(yaml@2.7.0): @@ -27679,9 +26672,9 @@ snapshots: tsx: 4.19.3 yaml: 2.7.0 - postcss-nested@6.2.0(postcss@8.4.49): + postcss-nested@6.2.0(postcss@8.5.3): dependencies: - postcss: 8.4.49 + postcss: 8.5.3 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.0.10: @@ -29105,6 +28098,8 @@ snapshots: sprintf-js@1.1.3: {} + sql-highlight@6.0.0: {} + sqlite3@5.1.7: dependencies: bindings: 1.5.0 @@ -29423,11 +28418,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.4.49 - postcss-import: 15.1.0(postcss@8.4.49) - postcss-js: 4.0.1(postcss@8.4.49) - postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)) - postcss-nested: 6.2.0(postcss@8.4.49) + postcss: 8.5.3 + postcss-import: 15.1.0(postcss@8.5.3) + postcss-js: 4.0.1(postcss@8.5.3) + postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)) + postcss-nested: 6.2.0(postcss@8.5.3) postcss-selector-parser: 6.1.2 resolve: 1.22.10 sucrase: 3.35.0 @@ -29840,28 +28835,27 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.20(mongodb@6.13.0(@aws-sdk/credential-providers@3.741.0(aws-crt@1.25.3))(socks@2.8.4))(mssql@11.0.1)(mysql2@3.12.0)(pg@8.13.1)(redis@4.7.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)): + typeorm@0.3.21(mongodb@6.15.0(@aws-sdk/credential-providers@3.774.0(aws-crt@1.25.3))(socks@2.8.4))(mssql@11.0.1)(mysql2@3.14.0)(pg@8.14.1)(redis@4.7.0)(reflect-metadata@0.2.2)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2)): dependencies: '@sqltools/formatter': 1.2.5 + ansis: 3.17.0 app-root-path: 3.1.0 buffer: 6.0.3 - chalk: 4.1.2 - cli-highlight: 2.1.11 dayjs: 1.11.13 debug: 4.4.0 dotenv: 16.4.7 glob: 10.4.5 - mkdirp: 2.1.6 reflect-metadata: 0.2.2 sha.js: 2.4.11 + sql-highlight: 6.0.0 tslib: 2.8.1 - uuid: 9.0.1 + uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: - mongodb: 6.13.0(@aws-sdk/credential-providers@3.741.0(aws-crt@1.25.3))(socks@2.8.4) + mongodb: 6.15.0(@aws-sdk/credential-providers@3.774.0(aws-crt@1.25.3))(socks@2.8.4) mssql: 11.0.1 - mysql2: 3.12.0 - pg: 8.13.1 + mysql2: 3.14.0 + pg: 8.14.1 redis: 4.7.0 sqlite3: 5.1.7 ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.8.2) @@ -30082,67 +29076,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.9(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.6.0 - pathe: 1.1.2 - vite: 5.4.15(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite-node@3.0.5(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.6.0 - pathe: 2.0.3 - vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-node@3.0.6(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.6.0 - pathe: 2.0.3 - vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-node@3.0.7(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): + vite-node@3.1.1(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 @@ -30163,6 +29097,27 @@ snapshots: - tsx - yaml + vite-node@3.1.1(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 2.0.3 + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.1.1(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: cac: 6.7.14 @@ -30184,20 +29139,20 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.3.0(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)): + vite-plugin-dts@4.5.3(@types/node@22.10.2)(rollup@4.37.0)(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)): dependencies: '@microsoft/api-extractor': 7.52.2(@types/node@22.10.2) '@rollup/pluginutils': 5.1.4(rollup@4.37.0) '@volar/typescript': 2.4.12 - '@vue/language-core': 2.1.6(typescript@5.8.2) + '@vue/language-core': 2.2.0(typescript@5.8.2) compare-versions: 6.1.1 debug: 4.4.0 kolorist: 1.8.0 - local-pkg: 0.5.1 + local-pkg: 1.1.1 magic-string: 0.30.17 typescript: 5.8.2 optionalDependencies: - vite: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - rollup @@ -30222,75 +29177,36 @@ snapshots: - rollup - supports-color - vite-plugin-node-polyfills@0.22.0(rollup@4.37.0)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)): + vite-plugin-node-polyfills@0.22.0(rollup@4.37.0)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.37.0) node-stdlib-browser: 1.3.1 - vite: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - rollup - vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)): + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.2) optionalDependencies: - vite: 6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.3(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)): + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.2) optionalDependencies: - vite: 6.2.3(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.15(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.49 - rollup: 4.37.0 - optionalDependencies: - '@types/node': 22.10.2 - fsevents: 2.3.3 - lightningcss: 1.29.2 - terser: 5.39.0 - - vite@6.0.12(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - esbuild: 0.24.2 - postcss: 8.4.49 - rollup: 4.37.0 - optionalDependencies: - '@types/node': 22.10.2 - fsevents: 2.3.3 - jiti: 2.4.2 - lightningcss: 1.29.2 - terser: 5.37.0 - tsx: 4.19.3 - yaml: 2.7.0 - - vite@6.2.3(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - esbuild: 0.25.2 - postcss: 8.5.3 - rollup: 4.37.0 - optionalDependencies: - '@types/node': 22.10.2 - fsevents: 2.3.3 - jiti: 2.4.1 - lightningcss: 1.29.2 - terser: 5.39.0 - tsx: 4.19.3 - yaml: 2.7.0 - vite@6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: esbuild: 0.25.2 @@ -30333,143 +29249,27 @@ snapshots: tsx: 4.19.3 yaml: 2.7.0 - vitest-mock-extended@2.0.2(typescript@5.8.2)(vitest@2.1.9(@types/node@22.10.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)): + vitest-mock-extended@3.0.1(typescript@5.8.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)): dependencies: ts-essentials: 10.0.4(typescript@5.8.2) typescript: 5.8.2 - vitest: 2.1.9(@types/node@22.10.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - vitest-mock-extended@2.0.2(typescript@5.8.2)(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)): + vitest-mock-extended@3.0.1(typescript@5.8.2)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)): dependencies: ts-essentials: 10.0.4(typescript@5.8.2) typescript: 5.8.2 - vitest: 3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - vitest@2.1.9(@types/node@22.10.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0): + vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.15(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.2.0 - debug: 4.4.0 - expect-type: 1.2.0 - magic-string: 0.30.17 - pathe: 1.1.2 - std-env: 3.8.1 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.0.2 - tinyrainbow: 1.2.0 - vite: 5.4.15(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0) - vite-node: 2.1.9(@types/node@22.10.2)(lightningcss@1.29.2)(terser@5.39.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 22.10.2 - jsdom: 25.0.1 - transitivePeerDependencies: - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) - '@vitest/pretty-format': 3.0.9 - '@vitest/runner': 3.0.5 - '@vitest/snapshot': 3.0.5 - '@vitest/spy': 3.0.5 - '@vitest/utils': 3.0.5 - chai: 5.2.0 - debug: 4.4.0 - expect-type: 1.2.0 - magic-string: 0.30.17 - pathe: 2.0.3 - std-env: 3.8.1 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.0.2 - tinyrainbow: 2.0.0 - vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - vite-node: 3.0.5(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 22.10.2 - jsdom: 25.0.1 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vitest@3.0.6(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - '@vitest/expect': 3.0.6 - '@vitest/mocker': 3.0.6(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) - '@vitest/pretty-format': 3.0.9 - '@vitest/runner': 3.0.6 - '@vitest/snapshot': 3.0.6 - '@vitest/spy': 3.0.6 - '@vitest/utils': 3.0.6 - chai: 5.2.0 - debug: 4.4.0 - expect-type: 1.2.0 - magic-string: 0.30.17 - pathe: 2.0.3 - std-env: 3.8.1 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.0.2 - tinyrainbow: 2.0.0 - vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - vite-node: 3.0.6(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 22.10.2 - jsdom: 25.0.1 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.1)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0): - dependencies: - '@vitest/expect': 3.0.7 - '@vitest/mocker': 3.0.7(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/expect': 3.1.1 + '@vitest/mocker': 3.1.1(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) '@vitest/pretty-format': 3.1.1 - '@vitest/runner': 3.0.7 - '@vitest/snapshot': 3.0.7 - '@vitest/spy': 3.0.7 - '@vitest/utils': 3.0.7 + '@vitest/runner': 3.1.1 + '@vitest/snapshot': 3.1.1 + '@vitest/spy': 3.1.1 + '@vitest/utils': 3.1.1 chai: 5.2.0 debug: 4.4.0 expect-type: 1.2.0 @@ -30481,7 +29281,47 @@ snapshots: tinypool: 1.0.2 tinyrainbow: 2.0.0 vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) - vite-node: 3.0.7(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + vite-node: 3.1.1(@types/node@22.10.2)(jiti@2.4.1)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.10.2 + jsdom: 25.0.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0): + dependencies: + '@vitest/expect': 3.1.1 + '@vitest/mocker': 3.1.1(vite@6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/pretty-format': 3.1.1 + '@vitest/runner': 3.1.1 + '@vitest/snapshot': 3.1.1 + '@vitest/spy': 3.1.1 + '@vitest/utils': 3.1.1 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.0 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.8.1 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.2.4(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) + vite-node: 3.1.1(@types/node@22.10.2)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.37.0)(tsx@4.19.3)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -30783,7 +29623,7 @@ snapshots: wmf: 1.0.2 word: 0.3.0 - xml-crypto@6.0.0: + xml-crypto@6.0.1: dependencies: '@xmldom/is-dom-node': 1.0.1 '@xmldom/xmldom': 0.8.10 @@ -30836,8 +29676,6 @@ snapshots: camelcase: 5.3.1 decamelize: 1.2.0 - yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} yargs@15.4.1: @@ -30854,16 +29692,6 @@ snapshots: y18n: 4.0.3 yargs-parser: 18.1.3 - yargs@16.2.0: - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - yargs@17.7.2: dependencies: cliui: 8.0.1 From 3fd5515db1b051bd4862d19abe9e85f3671f3a1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 05:03:40 +0200 Subject: [PATCH 141/411] chore(deps): bump SonarSource/sonarqube-scan-action from 4.2.1 to 5.1.0 (#5104) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sonarqube.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 2657ee8402..35fff1fdfe 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -48,7 +48,7 @@ jobs: run: | pnpm test:coverage - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 + uses: SonarSource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From f0c7b881d3796b88b218d29939f58d66858677a7 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:31:26 +0530 Subject: [PATCH 142/411] fix: don't allow spaces as "other" values in select questions (#5224) --- .../src/components/questions/multiple-choice-multi-question.tsx | 1 + .../src/components/questions/multiple-choice-single-question.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx b/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx index dc46955feb..59ceb06786 100644 --- a/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx @@ -279,6 +279,7 @@ export function MultipleChoiceMultiQuestion({ } required={question.required} aria-labelledby={`${otherOption.id}-label`} + pattern=".*\S+.*" /> ) : null} diff --git a/packages/surveys/src/components/questions/multiple-choice-single-question.tsx b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx index bac793834e..b5d723a9de 100644 --- a/packages/surveys/src/components/questions/multiple-choice-single-question.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx @@ -237,6 +237,7 @@ export function MultipleChoiceSingleQuestion({ } required={question.required} aria-labelledby={`${otherOption.id}-label`} + pattern=".*\S+.*" /> ) : null} From 591b35a70b967f3be56f7219abc73ada9f9a27eb Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Sat, 5 Apr 2025 13:04:01 +0900 Subject: [PATCH 143/411] fix: upgrade npm dependencies with high security risk (#5221) --- apps/demo-react-native/package.json | 2 +- apps/web/package.json | 2 +- package.json | 2 +- playwright.config.ts | 2 + pnpm-lock.yaml | 510 ++++++++++++++++------------ 5 files changed, 299 insertions(+), 219 deletions(-) diff --git a/apps/demo-react-native/package.json b/apps/demo-react-native/package.json index acd06c3451..3c4cae2b8e 100644 --- a/apps/demo-react-native/package.json +++ b/apps/demo-react-native/package.json @@ -18,7 +18,7 @@ "expo-status-bar": "2.0.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-native": "0.76.6", + "react-native": "0.78.2", "react-native-webview": "13.12.5" }, "devDependencies": { diff --git a/apps/web/package.json b/apps/web/package.json index 6a5035da23..d882913fc1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -68,7 +68,7 @@ "@radix-ui/react-toggle": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-tooltip": "1.1.5", - "@react-email/components": "0.0.31", + "@react-email/components": "0.0.35", "@sentry/nextjs": "8.52.0", "@tailwindcss/forms": "0.5.9", "@tailwindcss/typography": "0.5.15", diff --git a/package.json b/package.json index cb395a3d51..ef1783cd8e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "lint-staged": "15.5.0", "rimraf": "6.0.1", "tsx": "4.19.3", - "turbo": "2.4.4" + "turbo": "2.5.0" }, "lint-staged": { "(apps|packages)/**/*.{js,ts,jsx,tsx}": [ diff --git a/playwright.config.ts b/playwright.config.ts index b1362e8a94..c5ab28444a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,6 +17,8 @@ export default defineConfig({ retries: 0, /* Timeout for each test */ timeout: 120000, + /* Fail the test run after the first failure */ + maxFailures: 1, // Stop execution after the first failed test /* Opt out of parallel tests on CI. */ // workers: os.cpus().length, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e42e74d42..2768aebcb2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ importers: specifier: 4.19.3 version: 4.19.3 turbo: - specifier: 2.4.4 - version: 2.4.4 + specifier: 2.5.0 + version: 2.5.0 apps/demo: dependencies: @@ -84,13 +84,13 @@ importers: version: link:../../packages/react-native '@react-native-async-storage/async-storage': specifier: 2.1.0 - version: 2.1.0(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1)) + version: 2.1.0(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1)) expo: specifier: 52.0.28 - version: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + version: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) expo-status-bar: specifier: 2.0.1 - version: 2.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + version: 2.0.1(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -98,11 +98,11 @@ importers: specifier: 18.3.1 version: 18.3.1(react@18.3.1) react-native: - specifier: 0.76.6 - version: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + specifier: 0.78.2 + version: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) react-native-webview: specifier: 13.12.5 - version: 13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + version: 13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) devDependencies: '@babel/core': specifier: 7.26.0 @@ -343,8 +343,8 @@ importers: specifier: 1.1.5 version: 1.1.5(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@react-email/components': - specifier: 0.0.31 - version: 0.0.31(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 0.0.35 + version: 0.0.35(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@sentry/nextjs': specifier: 8.52.0 version: 8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.25.2)) @@ -4155,8 +4155,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/components@0.0.31': - resolution: {integrity: sha512-rQsTY9ajobncix9raexhBjC7O6cXUMc87eNez2gnB1FwtkUO8DqWZcktbtwOJi7GKmuAPTx0o/IOFtiBNXziKA==} + '@react-email/components@0.0.35': + resolution: {integrity: sha512-if1kLih4pfARgsXacs9eD9O3BVtRWxKRz1jjSWWiyk32eeFJLtWjBaoF8nsxQxk4w5nfqjAHVFBrxXQceB7xDQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -4220,8 +4220,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/render@1.0.3': - resolution: {integrity: sha512-VQ8g4SuIq/jWdfBTdTjb7B8Np0jj+OoD7VebfdHhLTZzVQKesR2aigpYqE/ZXmwj4juVxDm8T2b6WIIu48rPCg==} + '@react-email/render@1.0.5': + resolution: {integrity: sha512-CA69HYXPk21HhtAXATIr+9JJwpDNmAFCvdMUjWmeoD1+KhJ9NAxusMRxKNeibdZdslmq3edaeOKGbdQ9qjK8LQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -4245,8 +4245,8 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/text@0.0.11': - resolution: {integrity: sha512-a7nl/2KLpRHOYx75YbYZpWspUbX1DFY7JIZbOv5x0QU8SvwDbJt+Hm01vG34PffFyYvHEXrc6Qnip2RTjljNjg==} + '@react-email/text@0.1.1': + resolution: {integrity: sha512-Zo9tSEzkO3fODLVH1yVhzVCiwETfeEL5wU93jXKWo2DHoMuiZ9Iabaso3T0D0UjhrCB1PBMeq2YiejqeToTyIQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc @@ -4303,48 +4303,42 @@ packages: resolution: {integrity: sha512-1XmRhqQchN+pXPKEKYdpJlwESxVomJOxtEnIkbo7GAlaN2sym84fHEGDXAjLilih5GVPpcpSmFzTy8jx3LtaFg==} engines: {node: '>=18'} - '@react-native/assets-registry@0.76.6': - resolution: {integrity: sha512-YI8HoReYiIwdFQs+k9Q9qpFTnsyYikZxgs/UVtVbhKixXDQF6F9LLvj2naOx4cfV+RGybNKxwmDl1vUok/dRFQ==} + '@react-native/assets-registry@0.78.2': + resolution: {integrity: sha512-VHqQqjj1rnh2KQeS3yx4IfFSxIIIDi1jR4yUeC438Q6srwxDohR4W0UkXuSIz0imhlems5eS7yZTjdgSpWHRUQ==} engines: {node: '>=18'} '@react-native/babel-plugin-codegen@0.74.87': resolution: {integrity: sha512-+vJYpMnENFrwtgvDfUj+CtVJRJuUnzAUYT0/Pb68Sq9RfcZ5xdcCuUgyf7JO+akW2VTBoJY427wkcxU30qrWWw==} engines: {node: '>=18'} - '@react-native/babel-plugin-codegen@0.76.6': - resolution: {integrity: sha512-yFC9I/aDBOBz3ZMlqKn2NY/mDUtCksUNZ7AQmBiTAeVTUP0ujEjE0hTOx5Qd+kok7A7hwZEX87HdSgjiJZfr5g==} - engines: {node: '>=18'} - '@react-native/babel-plugin-codegen@0.76.7': resolution: {integrity: sha512-+8H4DXJREM4l/pwLF/wSVMRzVhzhGDix5jLezNrMD9J1U1AMfV2aSkWA1XuqR7pjPs/Vqf6TaPL7vJMZ4LU05Q==} engines: {node: '>=18'} + '@react-native/babel-plugin-codegen@0.78.2': + resolution: {integrity: sha512-0MnQOhIaOdWbQ3Dx3dz0MBbG+1ggBiyUL+Y+xHAeSDSaiRATT8DIsrSloeJU0A+2p5TxF8ITJyJ6KEQkMyB/Zw==} + engines: {node: '>=18'} + '@react-native/babel-preset@0.74.87': resolution: {integrity: sha512-hyKpfqzN2nxZmYYJ0tQIHG99FQO0OWXp/gVggAfEUgiT+yNKas1C60LuofUsK7cd+2o9jrpqgqW4WzEDZoBlTg==} engines: {node: '>=18'} peerDependencies: '@babel/core': '*' - '@react-native/babel-preset@0.76.6': - resolution: {integrity: sha512-ojlVWY6S/VE/nb9hIRetPMTsW9ZmGb2R3dnToEXAtQQDz41eHMHXbkw/k2h0THp6qhas25ruNvn3N5n2o+lBzg==} - engines: {node: '>=18'} - peerDependencies: - '@babel/core': '*' - '@react-native/babel-preset@0.76.7': resolution: {integrity: sha512-/c5DYZ6y8tyg+g8tgXKndDT7mWnGmkZ9F+T3qNDfoE3Qh7ucrNeC2XWvU9h5pk8eRtj9l4SzF4aO1phzwoibyg==} engines: {node: '>=18'} peerDependencies: '@babel/core': '*' - '@react-native/codegen@0.74.87': - resolution: {integrity: sha512-GMSYDiD+86zLKgMMgz9z0k6FxmRn+z6cimYZKkucW4soGbxWsbjUAZoZ56sJwt2FJ3XVRgXCrnOCgXoH/Bkhcg==} + '@react-native/babel-preset@0.78.2': + resolution: {integrity: sha512-VGOLhztQY/0vktMXrBr01HUN/iBSdkKBRiiZYfrLqx9fB2ql55gZb/6X9lzItjVyYoOc2jyHXSX8yoSfDcWDZg==} engines: {node: '>=18'} peerDependencies: - '@babel/preset-env': ^7.1.6 + '@babel/core': '*' - '@react-native/codegen@0.76.6': - resolution: {integrity: sha512-BABb3e5G/+hyQYEYi0AODWh2km2d8ERoASZr6Hv90pVXdUHRYR+yxCatX7vSd9rnDUYndqRTzD0hZWAucPNAKg==} + '@react-native/codegen@0.74.87': + resolution: {integrity: sha512-GMSYDiD+86zLKgMMgz9z0k6FxmRn+z6cimYZKkucW4soGbxWsbjUAZoZ56sJwt2FJ3XVRgXCrnOCgXoH/Bkhcg==} engines: {node: '>=18'} peerDependencies: '@babel/preset-env': ^7.1.6 @@ -4355,17 +4349,23 @@ packages: peerDependencies: '@babel/preset-env': ^7.1.6 + '@react-native/codegen@0.78.2': + resolution: {integrity: sha512-4r3/W1h22/GAmAMuMRMJWsw/9JGUEDAnSbYNya7zID1XSvizLoA5Yn8Qv+phrRwwsl0eZLxOqONh/nzXJcvpyg==} + engines: {node: '>=18'} + peerDependencies: + '@babel/preset-env': ^7.1.6 + '@react-native/community-cli-plugin@0.74.87': resolution: {integrity: sha512-EgJG9lSr8x3X67dHQKQvU6EkO+3ksVlJHYIVv6U/AmW9dN80BEFxgYbSJ7icXS4wri7m4kHdgeq2PQ7/3vvrTQ==} engines: {node: '>=18'} - '@react-native/community-cli-plugin@0.76.6': - resolution: {integrity: sha512-nETlc/+U5cESVluzzgN0OcVfcoMijGBaDWzOaJhoYUodcuqnqtu75XsSEc7yzlYjwNQG+vF83mu9CQGezruNMA==} + '@react-native/community-cli-plugin@0.78.2': + resolution: {integrity: sha512-xqEnpqxvBlm02mRY58L0NBjF25MTHmbaeA2qBx5VtheH/pXL6MHUbtwB1Q2dJrg9XcK0Np1i9h7N5h9gFwA2Mg==} engines: {node: '>=18'} peerDependencies: - '@react-native-community/cli-server-api': '*' + '@react-native-community/cli': '*' peerDependenciesMeta: - '@react-native-community/cli-server-api': + '@react-native-community/cli': optional: true '@react-native/debugger-frontend@0.74.87': @@ -4376,6 +4376,10 @@ packages: resolution: {integrity: sha512-kP97xMQjiANi5/lmf8MakS7d8FTJl+BqYHQMqyvNiY+eeWyKnhqW2GL2v3eEUBAuyPBgJGivuuO4RvjZujduJg==} engines: {node: '>=18'} + '@react-native/debugger-frontend@0.78.2': + resolution: {integrity: sha512-qNJT679OU/cdAKmZxfBFjqTG+ZC5i/4sLyvbcQjFFypunGSOaWl3mMQFQQdCBIQN+DFDPVSUXTPZQK1uI2j/ow==} + engines: {node: '>=18'} + '@react-native/dev-middleware@0.74.87': resolution: {integrity: sha512-7TmZ3hTHwooYgIHqc/z87BMe1ryrIqAUi+AF7vsD+EHCGxHFdMjSpf1BZ2SUPXuLnF2cTiTfV2RwhbPzx0tYIA==} engines: {node: '>=18'} @@ -4384,20 +4388,24 @@ packages: resolution: {integrity: sha512-1bAyd2/X48Nzb45s5l2omM75vy764odx/UnDs4sJfFCuK+cupU4nRPgl0XWIqgdM/2+fbQ3E4QsVS/WIKTFxvQ==} engines: {node: '>=18'} + '@react-native/dev-middleware@0.78.2': + resolution: {integrity: sha512-/u0pGiWVgvx09cYNO4/Okj8v1ZNt4K941pQJPhdwg5AHYuggVHNJjROukXJzZiElYFcJhMfOuxwksiIyx/GAkA==} + engines: {node: '>=18'} + '@react-native/gradle-plugin@0.74.87': resolution: {integrity: sha512-T+VX0N1qP+U9V4oAtn7FTX7pfsoVkd1ocyw9swYXgJqU2fK7hC9famW7b3s3ZiufPGPr1VPJe2TVGtSopBjL6A==} engines: {node: '>=18'} - '@react-native/gradle-plugin@0.76.6': - resolution: {integrity: sha512-sDzpf4eiynryoS6bpYCweGoxSmWgCSx9lzBoxIIW+S6siyGiTaffzZHWCm8mIn9UZsSPlEO37q62ggnR9Zu/OA==} + '@react-native/gradle-plugin@0.78.2': + resolution: {integrity: sha512-LHgmdrbyK9fcBDdxtn2GLOoDAE+aFHtDHgu6vUZ5CSCi9CMd5Krq8IWAmWjeq+BQr+D1rwSXDAHtOrfJ6qOolA==} engines: {node: '>=18'} '@react-native/js-polyfills@0.74.87': resolution: {integrity: sha512-M5Evdn76CuVEF0GsaXiGi95CBZ4IWubHqwXxV9vG9CC9kq0PSkoM2Pn7Lx7dgyp4vT7ccJ8a3IwHbe+5KJRnpw==} engines: {node: '>=18'} - '@react-native/js-polyfills@0.76.6': - resolution: {integrity: sha512-cDD7FynxWYxHkErZzAJtzPGhJ13JdOgL+R0riTh0hCovOfIUz9ItffdLQv2nx48lnvMTQ+HZXMnGOZnsFCNzQw==} + '@react-native/js-polyfills@0.78.2': + resolution: {integrity: sha512-b7eCPAs3uogdDeTvOTrU6i8DTTsHyjyp48R5pVakJIREhEx+SkUnlVk11PYjbCKGYjYgN939Tb5b1QWNtdrPIQ==} engines: {node: '>=18'} '@react-native/metro-babel-transformer@0.74.87': @@ -4406,8 +4414,8 @@ packages: peerDependencies: '@babel/core': '*' - '@react-native/metro-babel-transformer@0.76.6': - resolution: {integrity: sha512-xSBi9jPliThu5HRSJvluqUlDOLLEmf34zY/U7RDDjEbZqC0ufPcPS7c5XsSg0GDPiXc7lgjBVesPZsKFkoIBgA==} + '@react-native/metro-babel-transformer@0.78.2': + resolution: {integrity: sha512-H4614LjcbrG+lUtg+ysMX5RnovY8AwrWj4rH8re6ErfhPFwLQXV0LIrl/fgFpq07Vjc5e3ZXzuKuMJF6l7eeTQ==} engines: {node: '>=18'} peerDependencies: '@babel/core': '*' @@ -4415,12 +4423,12 @@ packages: '@react-native/normalize-colors@0.74.87': resolution: {integrity: sha512-Xh7Nyk/MPefkb0Itl5Z+3oOobeG9lfLb7ZOY2DKpFnoCE1TzBmib9vMNdFaLdSxLIP+Ec6icgKtdzYg8QUPYzA==} - '@react-native/normalize-colors@0.76.6': - resolution: {integrity: sha512-1n4udXH2Cla31iA/8eLRdhFHpYUYK1NKWCn4m1Sr9L4SarWKAYuRFliK1fcLvPPALCFoFlWvn8I0ekdUOHMzDQ==} - '@react-native/normalize-colors@0.76.7': resolution: {integrity: sha512-ST1xxBuYVIXPdD81dR6+tzIgso7m3pa9+6rOBXTh5Xm7KEEFik7tnQX+GydXYMp3wr1gagJjragdXkPnxK6WNg==} + '@react-native/normalize-colors@0.78.2': + resolution: {integrity: sha512-CA/3ynRO6/g1LDbqU8ewrv0js/1lU4+j04L7qz6btXbLTDk1UkF+AfpGRJGbIVY9UmFBJ7l1AOmzwutrWb3Txw==} + '@react-native/virtualized-lists@0.74.87': resolution: {integrity: sha512-lsGxoFMb0lyK/MiplNKJpD+A1EoEUumkLrCjH4Ht+ZlG8S0BfCxmskLZ6qXn3BiDSkLjfjI/qyZ3pnxNBvkXpQ==} engines: {node: '>=18'} @@ -4432,11 +4440,11 @@ packages: '@types/react': optional: true - '@react-native/virtualized-lists@0.76.6': - resolution: {integrity: sha512-0HUWVwJbRq1BWFOu11eOWGTSmK9nMHhoMPyoI27wyWcl/nqUx7HOxMbRVq0DsTCyATSMPeF+vZ6o1REapcNWKw==} + '@react-native/virtualized-lists@0.78.2': + resolution: {integrity: sha512-y/wVRUz1ImR2hKKUXFroTdSBiL0Dd+oudzqcGKp/M8Ybrw9MQ0m2QCXxtyONtDn8qkEGceqllwTCKq5WQwJcew==} engines: {node: '>=18'} peerDependencies: - '@types/react': ^18.2.6 + '@types/react': ^19.0.0 react: '*' react-native: '*' peerDependenciesMeta: @@ -6404,9 +6412,6 @@ packages: babel-plugin-react-native-web@0.19.13: resolution: {integrity: sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==} - babel-plugin-syntax-hermes-parser@0.23.1: - resolution: {integrity: sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA==} - babel-plugin-syntax-hermes-parser@0.25.1: resolution: {integrity: sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==} @@ -9154,6 +9159,16 @@ packages: peerDependencies: '@babel/preset-env': ^7.1.6 + jscodeshift@17.3.0: + resolution: {integrity: sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + '@babel/preset-env': ^7.1.6 + peerDependenciesMeta: + '@babel/preset-env': + optional: true + jsdoc-type-pratt-parser@4.1.0: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} @@ -11097,8 +11112,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} engines: {node: '>=14'} hasBin: true @@ -11334,6 +11349,9 @@ packages: react-devtools-core@5.3.2: resolution: {integrity: sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==} + react-devtools-core@6.1.1: + resolution: {integrity: sha512-TFo1MEnkqE6hzAbaztnyR5uLTMoz6wnEWwWBsCUzNt+sVXJycuRJdDqvL078M4/h65BI/YO5XWTaxZDWVsW0fw==} + react-docgen-typescript@2.2.2: resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: @@ -11427,13 +11445,13 @@ packages: '@types/react': optional: true - react-native@0.76.6: - resolution: {integrity: sha512-AsRi+ud6v6ADH7ZtSOY42kRB4nbM0KtSu450pGO4pDudl4AEK/AF96ai88snb2/VJJSGGa/49QyJVFXxz/qoFg==} + react-native@0.78.2: + resolution: {integrity: sha512-UilZ8sP9amHCz7TTMWMJ71JeYcMzEdgCJaqTfoB1hC/nYMXq6xqSFxKWCDhf7sR7nz3FKxS4t338t42AMDDkww==} engines: {node: '>=18'} hasBin: true peerDependencies: - '@types/react': ^18.2.6 - react: ^18.2.0 + '@types/react': ^19.0.0 + react: ^19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -12591,6 +12609,10 @@ packages: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} engines: {node: '>=8.17.0'} + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -12735,38 +12757,38 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - turbo-darwin-64@2.4.4: - resolution: {integrity: sha512-5kPvRkLAfmWI0MH96D+/THnDMGXlFNmjeqNRj5grLKiry+M9pKj3pRuScddAXPdlxjO5Ptz06UNaOQrrYGTx1g==} + turbo-darwin-64@2.5.0: + resolution: {integrity: sha512-fP1hhI9zY8hv0idym3hAaXdPi80TLovmGmgZFocVAykFtOxF+GlfIgM/l4iLAV9ObIO4SUXPVWHeBZQQ+Hpjag==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.4.4: - resolution: {integrity: sha512-/gtHPqbGQXDFhrmy+Q/MFW2HUTUlThJ97WLLSe4bxkDrKHecDYhAjbZ4rN3MM93RV9STQb3Tqy4pZBtsd4DfCw==} + turbo-darwin-arm64@2.5.0: + resolution: {integrity: sha512-p9sYq7kXH7qeJwIQE86cOWv/xNqvow846l6c/qWc26Ib1ci5W7V0sI5thsrP3eH+VA0d+SHalTKg5SQXgNQBWA==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.4.4: - resolution: {integrity: sha512-SR0gri4k0bda56hw5u9VgDXLKb1Q+jrw4lM7WAhnNdXvVoep4d6LmnzgMHQQR12Wxl3KyWPbkz9d1whL6NTm2Q==} + turbo-linux-64@2.5.0: + resolution: {integrity: sha512-1iEln2GWiF3iPPPS1HQJT6ZCFXynJPd89gs9SkggH2EJsj3eRUSVMmMC8y6d7bBbhBFsiGGazwFIYrI12zs6uQ==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.4.4: - resolution: {integrity: sha512-COXXwzRd3vslQIfJhXUklgEqlwq35uFUZ7hnN+AUyXx7hUOLIiD5NblL+ETrHnhY4TzWszrbwUMfe2BYWtaPQg==} + turbo-linux-arm64@2.5.0: + resolution: {integrity: sha512-bKBcbvuQHmsX116KcxHJuAcppiiBOfivOObh2O5aXNER6mce7YDDQJy00xQQNp1DhEfcSV2uOsvb3O3nN2cbcA==} cpu: [arm64] os: [linux] - turbo-windows-64@2.4.4: - resolution: {integrity: sha512-PV9rYNouGz4Ff3fd6sIfQy5L7HT9a4fcZoEv8PKRavU9O75G7PoDtm8scpHU10QnK0QQNLbE9qNxOAeRvF0fJg==} + turbo-windows-64@2.5.0: + resolution: {integrity: sha512-9BCo8oQ7BO7J0K913Czbc3tw8QwLqn2nTe4E47k6aVYkM12ASTScweXPTuaPFP5iYXAT6z5Dsniw704Ixa5eGg==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.4.4: - resolution: {integrity: sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg==} + turbo-windows-arm64@2.5.0: + resolution: {integrity: sha512-OUHCV+ueXa3UzfZ4co/ueIHgeq9B2K48pZwIxKSm5VaLVuv8M13MhM7unukW09g++dpdrrE1w4IOVgxKZ0/exg==} cpu: [arm64] os: [win32] - turbo@2.4.4: - resolution: {integrity: sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ==} + turbo@2.5.0: + resolution: {integrity: sha512-PvSRruOsitjy6qdqwIIyolv99+fEn57gP6gn4zhsHTEcCYgXPhv6BAxzAjleS8XKpo+Y582vTTA9nuqYDmbRuA==} hasBin: true tween-functions@1.2.0: @@ -13390,6 +13412,10 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@6.2.3: resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} peerDependencies: @@ -18210,7 +18236,7 @@ snapshots: dependencies: react: 19.0.0 - '@react-email/components@0.0.31(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-email/components@0.0.35(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@react-email/body': 0.0.11(react@19.0.0) '@react-email/button': 0.0.19(react@19.0.0) @@ -18227,11 +18253,11 @@ snapshots: '@react-email/link': 0.0.12(react@19.0.0) '@react-email/markdown': 0.0.14(react@19.0.0) '@react-email/preview': 0.0.12(react@19.0.0) - '@react-email/render': 1.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-email/render': 1.0.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@react-email/row': 0.0.12(react@19.0.0) '@react-email/section': 0.0.16(react@19.0.0) '@react-email/tailwind': 1.0.4(react@19.0.0) - '@react-email/text': 0.0.11(react@19.0.0) + '@react-email/text': 0.1.1(react@19.0.0) react: 19.0.0 transitivePeerDependencies: - react-dom @@ -18277,10 +18303,10 @@ snapshots: dependencies: react: 19.0.0 - '@react-email/render@1.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-email/render@1.0.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: html-to-text: 9.0.5 - prettier: 3.3.3 + prettier: 3.4.2 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) react-promise-suspense: 0.3.4 @@ -18297,7 +18323,7 @@ snapshots: dependencies: react: 19.0.0 - '@react-email/text@0.0.11(react@19.0.0)': + '@react-email/text@0.1.1(react@19.0.0)': dependencies: react: 19.0.0 @@ -18306,10 +18332,10 @@ snapshots: merge-options: 3.0.4 react-native: 0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@types/react@18.3.1)(encoding@0.1.13)(react@18.3.1) - '@react-native-async-storage/async-storage@2.1.0(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))': + '@react-native-async-storage/async-storage@2.1.0(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))': dependencies: merge-options: 3.0.4 - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) '@react-native-community/cli-clean@13.6.9(encoding@0.1.13)': dependencies: @@ -18464,7 +18490,7 @@ snapshots: '@react-native/assets-registry@0.74.87': {} - '@react-native/assets-registry@0.76.6': {} + '@react-native/assets-registry@0.78.2': {} '@react-native/babel-plugin-codegen@0.74.87(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: @@ -18473,16 +18499,17 @@ snapshots: - '@babel/preset-env' - supports-color - '@react-native/babel-plugin-codegen@0.76.6(@babel/preset-env@7.26.9(@babel/core@7.26.0))': + '@react-native/babel-plugin-codegen@0.76.7(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: - '@react-native/codegen': 0.76.6(@babel/preset-env@7.26.9(@babel/core@7.26.0)) + '@react-native/codegen': 0.76.7(@babel/preset-env@7.26.9(@babel/core@7.26.0)) transitivePeerDependencies: - '@babel/preset-env' - supports-color - '@react-native/babel-plugin-codegen@0.76.7(@babel/preset-env@7.26.9(@babel/core@7.26.0))': + '@react-native/babel-plugin-codegen@0.78.2(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: - '@react-native/codegen': 0.76.7(@babel/preset-env@7.26.9(@babel/core@7.26.0)) + '@babel/traverse': 7.27.0 + '@react-native/codegen': 0.78.2(@babel/preset-env@7.26.9(@babel/core@7.26.0)) transitivePeerDependencies: - '@babel/preset-env' - supports-color @@ -18536,57 +18563,6 @@ snapshots: - '@babel/preset-env' - supports-color - '@react-native/babel-preset@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))': - dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-proposal-export-default-from': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-export-default-from': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.26.0) - '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.26.0) - '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.26.0) - '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.26.0) - '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.0) - '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.26.0) - '@babel/plugin-transform-runtime': 7.26.10(@babel/core@7.26.0) - '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.0) - '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) - '@babel/template': 7.27.0 - '@react-native/babel-plugin-codegen': 0.76.6(@babel/preset-env@7.26.9(@babel/core@7.26.0)) - babel-plugin-syntax-hermes-parser: 0.25.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.0) - react-refresh: 0.14.2 - transitivePeerDependencies: - - '@babel/preset-env' - - supports-color - '@react-native/babel-preset@0.76.7(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: '@babel/core': 7.26.0 @@ -18638,6 +18614,57 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/babel-preset@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-proposal-export-default-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-export-default-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.27.0(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-flow-strip-types': 7.26.5(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.26.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.27.0(@babel/core@7.26.0) + '@babel/plugin-transform-runtime': 7.26.10(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/template': 7.27.0 + '@react-native/babel-plugin-codegen': 0.78.2(@babel/preset-env@7.26.9(@babel/core@7.26.0)) + babel-plugin-syntax-hermes-parser: 0.25.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.0) + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + '@react-native/codegen@0.74.87(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: '@babel/parser': 7.27.0 @@ -18651,7 +18678,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native/codegen@0.76.6(@babel/preset-env@7.26.9(@babel/core@7.26.0))': + '@react-native/codegen@0.76.7(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: '@babel/parser': 7.27.0 '@babel/preset-env': 7.26.9(@babel/core@7.26.0) @@ -18665,15 +18692,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native/codegen@0.76.7(@babel/preset-env@7.26.9(@babel/core@7.26.0))': + '@react-native/codegen@0.78.2(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: '@babel/parser': 7.27.0 '@babel/preset-env': 7.26.9(@babel/core@7.26.0) glob: 7.2.3 - hermes-parser: 0.23.1 + hermes-parser: 0.25.1 invariant: 2.2.4 - jscodeshift: 0.14.0(@babel/preset-env@7.26.9(@babel/core@7.26.0)) - mkdirp: 0.5.6 + jscodeshift: 17.3.0(@babel/preset-env@7.26.9(@babel/core@7.26.0)) nullthrows: 1.1.1 yargs: 17.7.2 transitivePeerDependencies: @@ -18701,26 +18727,24 @@ snapshots: - supports-color - utf-8-validate - '@react-native/community-cli-plugin@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(encoding@0.1.13)': + '@react-native/community-cli-plugin@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))': dependencies: - '@react-native/dev-middleware': 0.76.6 - '@react-native/metro-babel-transformer': 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0)) + '@react-native/dev-middleware': 0.78.2 + '@react-native/metro-babel-transformer': 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0)) chalk: 4.1.2 - execa: 5.1.1 + debug: 2.6.9 invariant: 2.2.4 metro: 0.81.4 metro-config: 0.81.4 metro-core: 0.81.4 - node-fetch: 2.7.0(encoding@0.1.13) readline: 1.3.0 semver: 7.7.1 optionalDependencies: - '@react-native-community/cli-server-api': 13.6.9(encoding@0.1.13) + '@react-native-community/cli': 13.6.9(encoding@0.1.13) transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - bufferutil - - encoding - supports-color - utf-8-validate @@ -18728,6 +18752,8 @@ snapshots: '@react-native/debugger-frontend@0.76.6': {} + '@react-native/debugger-frontend@0.78.2': {} + '@react-native/dev-middleware@0.74.87(encoding@0.1.13)': dependencies: '@isaacs/ttlcache': 1.4.1 @@ -18767,13 +18793,32 @@ snapshots: - supports-color - utf-8-validate + '@react-native/dev-middleware@0.78.2': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.78.2 + chrome-launcher: 0.15.2 + chromium-edge-launcher: 0.2.0 + connect: 3.7.0 + debug: 2.6.9 + invariant: 2.2.4 + nullthrows: 1.1.1 + open: 7.4.2 + selfsigned: 2.4.1 + serve-static: 1.16.2 + ws: 6.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@react-native/gradle-plugin@0.74.87': {} - '@react-native/gradle-plugin@0.76.6': {} + '@react-native/gradle-plugin@0.78.2': {} '@react-native/js-polyfills@0.74.87': {} - '@react-native/js-polyfills@0.76.6': {} + '@react-native/js-polyfills@0.78.2': {} '@react-native/metro-babel-transformer@0.74.87(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: @@ -18785,11 +18830,11 @@ snapshots: - '@babel/preset-env' - supports-color - '@react-native/metro-babel-transformer@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))': + '@react-native/metro-babel-transformer@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))': dependencies: '@babel/core': 7.26.0 - '@react-native/babel-preset': 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0)) - hermes-parser: 0.23.1 + '@react-native/babel-preset': 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0)) + hermes-parser: 0.25.1 nullthrows: 1.1.1 transitivePeerDependencies: - '@babel/preset-env' @@ -18797,10 +18842,10 @@ snapshots: '@react-native/normalize-colors@0.74.87': {} - '@react-native/normalize-colors@0.76.6': {} - '@react-native/normalize-colors@0.76.7': {} + '@react-native/normalize-colors@0.78.2': {} + '@react-native/virtualized-lists@0.74.87(@types/react@18.3.1)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@types/react@18.3.1)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: invariant: 2.2.4 @@ -18810,12 +18855,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 - '@react-native/virtualized-lists@0.76.6(@types/react@18.3.18)(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': + '@react-native/virtualized-lists@0.78.2(@types/react@18.3.18)(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 18.3.1 - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) optionalDependencies: '@types/react': 18.3.18 @@ -21280,10 +21325,6 @@ snapshots: babel-plugin-react-native-web@0.19.13: {} - babel-plugin-syntax-hermes-parser@0.23.1: - dependencies: - hermes-parser: 0.23.1 - babel-plugin-syntax-hermes-parser@0.25.1: dependencies: hermes-parser: 0.25.1 @@ -23044,42 +23085,42 @@ snapshots: expect-type@1.2.0: {} - expo-asset@11.0.5(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + expo-asset@11.0.5(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1): dependencies: '@expo/image-utils': 0.6.5 - expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) - expo-constants: 17.0.8(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1)) + expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) + expo-constants: 17.0.8(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1)) invariant: 2.2.4 md5-file: 3.2.3 react: 18.3.1 - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) transitivePeerDependencies: - supports-color - expo-constants@17.0.8(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1)): + expo-constants@17.0.8(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1)): dependencies: '@expo/config': 10.0.11 '@expo/env': 0.4.2 - expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) transitivePeerDependencies: - supports-color - expo-file-system@18.0.12(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1)): + expo-file-system@18.0.12(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1)): dependencies: - expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) web-streams-polyfill: 3.3.3 - expo-font@13.0.4(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1): + expo-font@13.0.4(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) fontfaceobserver: 2.3.0 react: 18.3.1 - expo-keep-awake@14.0.3(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1): + expo-keep-awake@14.0.3(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + expo: 52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) react: 18.3.1 expo-modules-autolinking@2.0.7: @@ -23097,12 +23138,12 @@ snapshots: dependencies: invariant: 2.2.4 - expo-status-bar@2.0.1(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + expo-status-bar@2.0.1(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) - expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.27.0 '@expo/cli': 0.22.11(encoding@0.1.13) @@ -23112,20 +23153,20 @@ snapshots: '@expo/metro-config': 0.19.9 '@expo/vector-icons': 14.0.4 babel-preset-expo: 12.0.9(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0)) - expo-asset: 11.0.5(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) - expo-constants: 17.0.8(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1)) - expo-file-system: 18.0.12(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1)) - expo-font: 13.0.4(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) - expo-keep-awake: 14.0.3(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) + expo-asset: 11.0.5(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) + expo-constants: 17.0.8(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1)) + expo-file-system: 18.0.12(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1)) + expo-font: 13.0.4(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react@18.3.1) + expo-keep-awake: 14.0.3(expo@52.0.28(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1))(react@18.3.1) expo-modules-autolinking: 2.0.7 expo-modules-core: 2.2.0 fbemitter: 3.0.0(encoding@0.1.13) react: 18.3.1 - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) web-streams-polyfill: 3.3.3 whatwg-url-without-unicode: 8.0.0-3 optionalDependencies: - react-native-webview: 13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + react-native-webview: 13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' @@ -24454,6 +24495,31 @@ snapshots: transitivePeerDependencies: - supports-color + jscodeshift@17.3.0(@babel/preset-env@7.26.9(@babel/core@7.26.0)): + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.27.0 + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/preset-flow': 7.25.9(@babel/core@7.26.0) + '@babel/preset-typescript': 7.27.0(@babel/core@7.26.0) + '@babel/register': 7.25.9(@babel/core@7.26.0) + flow-parser: 0.265.3 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + neo-async: 2.6.2 + picocolors: 1.1.1 + recast: 0.23.11 + tmp: 0.2.3 + write-file-atomic: 5.0.1 + optionalDependencies: + '@babel/preset-env': 7.26.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + jsdoc-type-pratt-parser@4.1.0: {} jsdom@25.0.1: @@ -26773,7 +26839,7 @@ snapshots: prettier@2.8.8: {} - prettier@3.3.3: {} + prettier@3.4.2: {} prettier@3.5.3: {} @@ -27027,6 +27093,14 @@ snapshots: - bufferutil - utf-8-validate + react-devtools-core@6.1.1: + dependencies: + shell-quote: 1.8.2 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + react-docgen-typescript@2.2.2(typescript@5.8.2): dependencies: typescript: 5.8.2 @@ -27122,12 +27196,12 @@ snapshots: react: 18.3.1 react-native: 0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@types/react@18.3.1)(encoding@0.1.13)(react@18.3.1) - react-native-webview@13.12.5(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): + react-native-webview@13.12.5(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1): dependencies: escape-string-regexp: 4.0.0 invariant: 2.2.4 react: 18.3.1 - react-native: 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1) + react-native: 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1) react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@types/react@18.3.1)(encoding@0.1.13)(react@18.3.1): dependencies: @@ -27179,21 +27253,21 @@ snapshots: - supports-color - utf-8-validate - react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1): + react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1): dependencies: '@jest/create-cache-key-function': 29.7.0 - '@react-native/assets-registry': 0.76.6 - '@react-native/codegen': 0.76.6(@babel/preset-env@7.26.9(@babel/core@7.26.0)) - '@react-native/community-cli-plugin': 0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(encoding@0.1.13) - '@react-native/gradle-plugin': 0.76.6 - '@react-native/js-polyfills': 0.76.6 - '@react-native/normalize-colors': 0.76.6 - '@react-native/virtualized-lists': 0.76.6(@types/react@18.3.18)(react-native@0.76.6(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli-server-api@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + '@react-native/assets-registry': 0.78.2 + '@react-native/codegen': 0.78.2(@babel/preset-env@7.26.9(@babel/core@7.26.0)) + '@react-native/community-cli-plugin': 0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13)) + '@react-native/gradle-plugin': 0.78.2 + '@react-native/js-polyfills': 0.78.2 + '@react-native/normalize-colors': 0.78.2 + '@react-native/virtualized-lists': 0.78.2(@types/react@18.3.18)(react-native@0.78.2(@babel/core@7.26.0)(@babel/preset-env@7.26.9(@babel/core@7.26.0))(@react-native-community/cli@13.6.9(encoding@0.1.13))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 babel-jest: 29.7.0(@babel/core@7.26.0) - babel-plugin-syntax-hermes-parser: 0.23.1 + babel-plugin-syntax-hermes-parser: 0.25.1 base64-js: 1.5.1 chalk: 4.1.2 commander: 12.1.0 @@ -27202,19 +27276,17 @@ snapshots: glob: 7.2.3 invariant: 2.2.4 jest-environment-node: 29.7.0 - jsc-android: 250231.0.0 memoize-one: 5.2.1 metro-runtime: 0.81.4 metro-source-map: 0.81.4 - mkdirp: 0.5.6 nullthrows: 1.1.1 pretty-format: 29.7.0 promise: 8.3.0 react: 18.3.1 - react-devtools-core: 5.3.2 + react-devtools-core: 6.1.1 react-refresh: 0.14.2 regenerator-runtime: 0.13.11 - scheduler: 0.24.0-canary-efb381bbf-20230505 + scheduler: 0.25.0 semver: 7.7.1 stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 @@ -27225,9 +27297,8 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - - '@react-native-community/cli-server-api' + - '@react-native-community/cli' - bufferutil - - encoding - supports-color - utf-8-validate @@ -28616,6 +28687,8 @@ snapshots: dependencies: rimraf: 3.0.2 + tmp@0.2.3: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -28751,32 +28824,32 @@ snapshots: dependencies: safe-buffer: 5.2.1 - turbo-darwin-64@2.4.4: + turbo-darwin-64@2.5.0: optional: true - turbo-darwin-arm64@2.4.4: + turbo-darwin-arm64@2.5.0: optional: true - turbo-linux-64@2.4.4: + turbo-linux-64@2.5.0: optional: true - turbo-linux-arm64@2.4.4: + turbo-linux-arm64@2.5.0: optional: true - turbo-windows-64@2.4.4: + turbo-windows-64@2.5.0: optional: true - turbo-windows-arm64@2.4.4: + turbo-windows-arm64@2.5.0: optional: true - turbo@2.4.4: + turbo@2.5.0: optionalDependencies: - turbo-darwin-64: 2.4.4 - turbo-darwin-arm64: 2.4.4 - turbo-linux-64: 2.4.4 - turbo-linux-arm64: 2.4.4 - turbo-windows-64: 2.4.4 - turbo-windows-arm64: 2.4.4 + turbo-darwin-64: 2.5.0 + turbo-darwin-arm64: 2.5.0 + turbo-linux-64: 2.5.0 + turbo-linux-arm64: 2.5.0 + turbo-windows-64: 2.5.0 + turbo-windows-arm64: 2.5.0 tween-functions@1.2.0: {} @@ -29600,6 +29673,11 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + ws@6.2.3: dependencies: async-limiter: 1.0.1 From 9d9b3ac5436e06f0d52c053134f6c6363f598619 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:52:45 +0530 Subject: [PATCH 144/411] chore: added isActive to user model (#5211) Co-authored-by: Piyush Gupta --- apps/web/app/(app)/layout.tsx | 6 ++++++ apps/web/modules/auth/lib/authOptions.ts | 10 ++++++++++ apps/web/modules/auth/lib/mock-data.ts | 1 + apps/web/modules/auth/lib/user.ts | 1 + .../ee/role-management/tests/__mocks__/actions.mock.ts | 1 + .../ee/sso/lib/tests/__mock__/sso-handlers.mock.ts | 1 + .../teams/components/edit-memberships/members-info.tsx | 4 ++++ .../organization/settings/teams/lib/membership.ts | 2 ++ .../settings/teams/tests/__mocks__/actions.mock.ts | 1 + .../survey/components/template-list/lib/user.ts | 1 + .../20250402063013_add_is_active_to_user/migration.sql | 2 ++ packages/database/schema.prisma | 1 + packages/lib/survey/tests/__mock__/survey.mock.ts | 1 + packages/lib/user/service.ts | 1 + packages/types/memberships.ts | 1 + packages/types/user.ts | 2 ++ 16 files changed, 36 insertions(+) create mode 100644 packages/database/migration/20250402063013_add_is_active_to_user/migration.sql diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index fc4000225c..c1588ca4dc 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -1,6 +1,7 @@ import { FormbricksClient } from "@/app/(app)/components/FormbricksClient"; import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper"; import { authOptions } from "@/modules/auth/lib/authOptions"; +import { ClientLogout } from "@/modules/ui/components/client-logout"; import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay"; import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client"; import { ToasterClient } from "@/modules/ui/components/toaster-client"; @@ -13,6 +14,11 @@ const AppLayout = async ({ children }) => { const session = await getServerSession(authOptions); const user = session?.user?.id ? await getUser(session.user.id) : null; + // If user account is deactivated, log them out instead of rendering the app + if (user?.isActive === false) { + return ; + } + return ( <> diff --git a/apps/web/modules/auth/lib/authOptions.ts b/apps/web/modules/auth/lib/authOptions.ts index eb9fdd9cfe..66dbed7a69 100644 --- a/apps/web/modules/auth/lib/authOptions.ts +++ b/apps/web/modules/auth/lib/authOptions.ts @@ -61,6 +61,9 @@ export const authOptions: NextAuthOptions = { if (!user.password) { throw new Error("User has no password stored"); } + if (user.isActive === false) { + throw new Error("Your account is currently inactive. Please contact the organization admin."); + } const isValid = await verifyPassword(credentials.password, user.password); @@ -162,6 +165,10 @@ export const authOptions: NextAuthOptions = { throw new Error("Email already verified"); } + if (user.isActive === false) { + throw new Error("Your account is currently inactive. Please contact the organization admin."); + } + user = await updateUser(user.id, { emailVerified: new Date() }); // send new user to brevo after email verification @@ -187,6 +194,7 @@ export const authOptions: NextAuthOptions = { return { ...token, profile: { id: existingUser.id }, + isActive: existingUser.isActive, }; }, async session({ session, token }) { @@ -194,6 +202,8 @@ export const authOptions: NextAuthOptions = { session.user.id = token?.id; // @ts-expect-error session.user = token.profile; + // @ts-expect-error + session.user.isActive = token.isActive; return session; }, diff --git a/apps/web/modules/auth/lib/mock-data.ts b/apps/web/modules/auth/lib/mock-data.ts index 9a4bb14094..825a0c06f6 100644 --- a/apps/web/modules/auth/lib/mock-data.ts +++ b/apps/web/modules/auth/lib/mock-data.ts @@ -18,4 +18,5 @@ export const mockUser: TUser = { }, role: "other", locale: "en-US", + isActive: true, }; diff --git a/apps/web/modules/auth/lib/user.ts b/apps/web/modules/auth/lib/user.ts index b5d647dc9e..7f14241a68 100644 --- a/apps/web/modules/auth/lib/user.ts +++ b/apps/web/modules/auth/lib/user.ts @@ -58,6 +58,7 @@ export const getUserByEmail = reactCache(async (email: string) => locale: true, email: true, emailVerified: true, + isActive: true, }, }); diff --git a/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts b/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts index 692923edc8..510e16fc25 100644 --- a/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts +++ b/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts @@ -28,6 +28,7 @@ export const mockUser: TUser = { locale: "en-US", imageUrl: null, role: null, + isActive: true, }; // Mock session diff --git a/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts index a35d700c14..32703c23f0 100644 --- a/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts +++ b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts @@ -21,6 +21,7 @@ export const mockUser: TUser = { createdAt: new Date(), updatedAt: new Date(), objective: "improve_user_retention", + isActive: true, }; // Mock account data diff --git a/apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.tsx b/apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.tsx index 50e7c4d3ce..e5c6efb30a 100644 --- a/apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.tsx +++ b/apps/web/modules/organization/settings/teams/components/edit-memberships/members-info.tsx @@ -53,6 +53,10 @@ export const MembersInfo = ({ ); } + if (!member.isActive) { + return ; + } + return ; }; diff --git a/apps/web/modules/organization/settings/teams/lib/membership.ts b/apps/web/modules/organization/settings/teams/lib/membership.ts index c15981448d..7e5072d21c 100644 --- a/apps/web/modules/organization/settings/teams/lib/membership.ts +++ b/apps/web/modules/organization/settings/teams/lib/membership.ts @@ -29,6 +29,7 @@ export const getMembershipByOrganizationId = reactCache( select: { name: true, email: true, + isActive: true, }, }, userId: true, @@ -46,6 +47,7 @@ export const getMembershipByOrganizationId = reactCache( userId: member.userId, accepted: member.accepted, role: member.role, + isActive: member.user?.isActive || false, }; }); diff --git a/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts b/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts index 1a3ec8d079..5678a058b3 100644 --- a/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts +++ b/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts @@ -40,6 +40,7 @@ export const mockUser: TUser = { locale: "en-US", imageUrl: null, role: null, + isActive: true, }; // Mock session diff --git a/apps/web/modules/survey/components/template-list/lib/user.ts b/apps/web/modules/survey/components/template-list/lib/user.ts index 8719847c1a..7eac3c2eb5 100644 --- a/apps/web/modules/survey/components/template-list/lib/user.ts +++ b/apps/web/modules/survey/components/template-list/lib/user.ts @@ -27,6 +27,7 @@ export const updateUser = async (personId: string, data: TUserUpdateInput): Prom objective: true, notificationSettings: true, locale: true, + isActive: true, }, }); diff --git a/packages/database/migration/20250402063013_add_is_active_to_user/migration.sql b/packages/database/migration/20250402063013_add_is_active_to_user/migration.sql new file mode 100644 index 0000000000..e769a1d7c1 --- /dev/null +++ b/packages/database/migration/20250402063013_add_is_active_to_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index b7bf760a77..a759ce3998 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -872,6 +872,7 @@ model User { locale String @default("en-US") surveys Survey[] teamUsers TeamUser[] + isActive Boolean @default(true) @@index([email]) } diff --git a/packages/lib/survey/tests/__mock__/survey.mock.ts b/packages/lib/survey/tests/__mock__/survey.mock.ts index 7c8d4bdea1..8754c7fba3 100644 --- a/packages/lib/survey/tests/__mock__/survey.mock.ts +++ b/packages/lib/survey/tests/__mock__/survey.mock.ts @@ -135,6 +135,7 @@ export const mockUser: TUser = { }, role: "other", locale: "en-US", + isActive: true, }; export const mockPrismaPerson: Prisma.ContactGetPayload<{ diff --git a/packages/lib/user/service.ts b/packages/lib/user/service.ts index a801a34f5c..00901f7738 100644 --- a/packages/lib/user/service.ts +++ b/packages/lib/user/service.ts @@ -26,6 +26,7 @@ const responseSelection = { objective: true, notificationSettings: true, locale: true, + isActive: true, }; // function to retrive basic information about a user's user diff --git a/packages/types/memberships.ts b/packages/types/memberships.ts index 1b3a3123b2..8fa830298f 100644 --- a/packages/types/memberships.ts +++ b/packages/types/memberships.ts @@ -21,6 +21,7 @@ export const ZMember = z.object({ userId: z.string(), accepted: z.boolean(), role: ZOrganizationRole, + isActive: z.boolean(), }); export type TMember = z.infer; diff --git a/packages/types/user.ts b/packages/types/user.ts index 55d68b29c9..18f41bb52a 100644 --- a/packages/types/user.ts +++ b/packages/types/user.ts @@ -58,6 +58,7 @@ export const ZUser = z.object({ objective: ZUserObjective.nullable(), notificationSettings: ZUserNotificationSettings, locale: ZUserLocale, + isActive: z.boolean().default(true), }); export type TUser = z.infer; @@ -72,6 +73,7 @@ export const ZUserUpdateInput = z.object({ imageUrl: z.string().nullish(), notificationSettings: ZUserNotificationSettings.optional(), locale: ZUserLocale.optional(), + isActive: z.boolean().optional(), }); export type TUserUpdateInput = z.infer; From cbf2343143b93f1b2065dad33987e65f0756c184 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Sat, 5 Apr 2025 16:52:38 +0530 Subject: [PATCH 145/411] feat: lastLoginAt to user model (#5216) --- apps/web/modules/auth/lib/authOptions.ts | 12 +++++-- apps/web/modules/auth/lib/mock-data.ts | 1 + apps/web/modules/auth/lib/user.test.ts | 25 +++++++++++++- apps/web/modules/auth/lib/user.ts | 28 +++++++++++++++ .../tests/__mocks__/actions.mock.ts | 1 + apps/web/modules/ee/sso/lib/sso-handlers.ts | 2 +- .../lib/tests/__mock__/sso-handlers.mock.ts | 1 + .../ee/sso/lib/tests/sso-handlers.test.ts | 34 +++++++++---------- .../teams/tests/__mocks__/actions.mock.ts | 1 + .../components/template-list/lib/user.ts | 1 + .../migration.sql | 2 ++ packages/database/schema.prisma | 1 + .../lib/survey/tests/__mock__/survey.mock.ts | 1 + packages/lib/user/service.ts | 1 + packages/types/user.ts | 2 ++ 15 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 packages/database/migration/20250403042737_add_last_login_at_to_user/migration.sql diff --git a/apps/web/modules/auth/lib/authOptions.ts b/apps/web/modules/auth/lib/authOptions.ts index 66dbed7a69..db9d31e98a 100644 --- a/apps/web/modules/auth/lib/authOptions.ts +++ b/apps/web/modules/auth/lib/authOptions.ts @@ -1,7 +1,7 @@ -import { getUserByEmail, updateUser } from "@/modules/auth/lib/user"; +import { getUserByEmail, updateUser, updateUserLastLoginAt } from "@/modules/auth/lib/user"; import { verifyPassword } from "@/modules/auth/lib/utils"; import { getSSOProviders } from "@/modules/ee/sso/lib/providers"; -import { handleSSOCallback } from "@/modules/ee/sso/lib/sso-handlers"; +import { handleSsoCallback } from "@/modules/ee/sso/lib/sso-handlers"; import type { Account, NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { prisma } from "@formbricks/database"; @@ -213,11 +213,17 @@ export const authOptions: NextAuthOptions = { if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { throw new Error("Email Verification is Pending"); } + await updateUserLastLoginAt(user.email); return true; } if (ENTERPRISE_LICENSE_KEY) { - return handleSSOCallback({ user, account }); + const result = await handleSsoCallback({ user, account }); + if (result) { + await updateUserLastLoginAt(user.email); + } + return result; } + await updateUserLastLoginAt(user.email); return true; }, }, diff --git a/apps/web/modules/auth/lib/mock-data.ts b/apps/web/modules/auth/lib/mock-data.ts index 825a0c06f6..3dfd70c011 100644 --- a/apps/web/modules/auth/lib/mock-data.ts +++ b/apps/web/modules/auth/lib/mock-data.ts @@ -18,5 +18,6 @@ export const mockUser: TUser = { }, role: "other", locale: "en-US", + lastLoginAt: new Date("2024-01-01T00:00:00.000Z"), isActive: true, }; diff --git a/apps/web/modules/auth/lib/user.test.ts b/apps/web/modules/auth/lib/user.test.ts index 10a7f6b984..93cd4951e8 100644 --- a/apps/web/modules/auth/lib/user.test.ts +++ b/apps/web/modules/auth/lib/user.test.ts @@ -5,7 +5,7 @@ import { PrismaErrorType } from "@formbricks/database/types/error"; import { userCache } from "@formbricks/lib/user/cache"; import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; import { mockUser } from "./mock-data"; -import { createUser, getUser, getUserByEmail, updateUser } from "./user"; +import { createUser, getUser, getUserByEmail, updateUser, updateUserLastLoginAt } from "./user"; const mockPrismaUser = { ...mockUser, @@ -96,6 +96,29 @@ describe("User Management", () => { }); }); + describe("updateUserLastLoginAt", () => { + const mockUpdateData = { name: "Updated Name" }; + + it("updates a user successfully", async () => { + vi.mocked(prisma.user.update).mockResolvedValueOnce({ ...mockPrismaUser, name: mockUpdateData.name }); + + const result = await updateUserLastLoginAt(mockUser.email); + + expect(result).toEqual(void 0); + expect(userCache.revalidate).toHaveBeenCalled(); + }); + + it("throws ResourceNotFoundError when user doesn't exist", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", { + code: PrismaErrorType.RecordDoesNotExist, + clientVersion: "0.0.1", + }); + vi.mocked(prisma.user.update).mockRejectedValueOnce(errToThrow); + + await expect(updateUserLastLoginAt(mockUser.email)).rejects.toThrow(ResourceNotFoundError); + }); + }); + describe("getUserByEmail", () => { const mockEmail = "test@example.com"; diff --git a/apps/web/modules/auth/lib/user.ts b/apps/web/modules/auth/lib/user.ts index 7f14241a68..3a19a0f7b3 100644 --- a/apps/web/modules/auth/lib/user.ts +++ b/apps/web/modules/auth/lib/user.ts @@ -43,6 +43,34 @@ export const updateUser = async (id: string, data: TUserUpdateInput) => { } }; +export const updateUserLastLoginAt = async (email: string) => { + validateInputs([email, ZUserEmail]); + + try { + const updatedUser = await prisma.user.update({ + where: { + email, + }, + data: { + lastLoginAt: new Date(), + }, + }); + + userCache.revalidate({ + email: updatedUser.email, + id: updatedUser.id, + }); + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === PrismaErrorType.RecordDoesNotExist + ) { + throw new ResourceNotFoundError("email", email); + } + throw error; + } +}; + export const getUserByEmail = reactCache(async (email: string) => cache( async () => { diff --git a/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts b/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts index 510e16fc25..e645309ddb 100644 --- a/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts +++ b/apps/web/modules/ee/role-management/tests/__mocks__/actions.mock.ts @@ -28,6 +28,7 @@ export const mockUser: TUser = { locale: "en-US", imageUrl: null, role: null, + lastLoginAt: new Date(), isActive: true, }; diff --git a/apps/web/modules/ee/sso/lib/sso-handlers.ts b/apps/web/modules/ee/sso/lib/sso-handlers.ts index 9ec9557e74..35bb9920a0 100644 --- a/apps/web/modules/ee/sso/lib/sso-handlers.ts +++ b/apps/web/modules/ee/sso/lib/sso-handlers.ts @@ -13,7 +13,7 @@ import { createOrganization, getOrganization } from "@formbricks/lib/organizatio import { findMatchingLocale } from "@formbricks/lib/utils/locale"; import type { TUser, TUserNotificationSettings } from "@formbricks/types/user"; -export const handleSSOCallback = async ({ user, account }: { user: TUser; account: Account }) => { +export const handleSsoCallback = async ({ user, account }: { user: TUser; account: Account }) => { const isSsoEnabled = await getisSsoEnabled(); if (!isSsoEnabled) { return false; diff --git a/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts index 32703c23f0..eb319298f7 100644 --- a/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts +++ b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts @@ -21,6 +21,7 @@ export const mockUser: TUser = { createdAt: new Date(), updatedAt: new Date(), objective: "improve_user_retention", + lastLoginAt: new Date(), isActive: true, }; diff --git a/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts b/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts index 0941a52ac3..318a4a9143 100644 --- a/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts +++ b/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts @@ -7,7 +7,7 @@ import { createAccount } from "@formbricks/lib/account/service"; import { createMembership } from "@formbricks/lib/membership/service"; import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; -import { handleSSOCallback } from "../sso-handlers"; +import { handleSsoCallback } from "../sso-handlers"; import { mockAccount, mockCreatedUser, @@ -65,7 +65,7 @@ vi.mock("@formbricks/lib/constants", () => ({ DEFAULT_ORGANIZATION_ROLE: "member", })); -describe("handleSSOCallback", () => { +describe("handleSsoCallback", () => { beforeEach(() => { vi.clearAllMocks(); @@ -90,7 +90,7 @@ describe("handleSSOCallback", () => { it("should return false if SSO is not enabled", async () => { vi.mocked(getisSsoEnabled).mockResolvedValue(false); - const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); expect(result).toBe(false); expect(getisSsoEnabled).toHaveBeenCalled(); @@ -99,7 +99,7 @@ describe("handleSSOCallback", () => { it("should return false if user email is missing", async () => { const userWithoutEmail = { ...mockUser, email: "" }; - const result = await handleSSOCallback({ user: userWithoutEmail, account: mockAccount }); + const result = await handleSsoCallback({ user: userWithoutEmail, account: mockAccount }); expect(result).toBe(false); }); @@ -107,7 +107,7 @@ describe("handleSSOCallback", () => { it("should return false if account type is not oauth", async () => { const nonOauthAccount = { ...mockAccount, type: "credentials" as const }; - const result = await handleSSOCallback({ user: mockUser, account: nonOauthAccount }); + const result = await handleSsoCallback({ user: mockUser, account: nonOauthAccount }); expect(result).toBe(false); }); @@ -115,7 +115,7 @@ describe("handleSSOCallback", () => { it("should return false if provider is SAML and SAML SSO is not enabled", async () => { vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(false); - const result = await handleSSOCallback({ user: mockUser, account: mockSamlAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockSamlAccount }); expect(result).toBe(false); expect(getIsSamlSsoEnabled).toHaveBeenCalled(); @@ -130,7 +130,7 @@ describe("handleSSOCallback", () => { accounts: [{ provider: mockAccount.provider }], }); - const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); expect(result).toBe(true); expect(prisma.user.findFirst).toHaveBeenCalledWith({ @@ -160,7 +160,7 @@ describe("handleSSOCallback", () => { vi.mocked(getUserByEmail).mockResolvedValue(null); vi.mocked(updateUser).mockResolvedValue({ ...existingUser, email: mockUser.email }); - const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); expect(result).toBe(true); expect(updateUser).toHaveBeenCalledWith(existingUser.id, { email: mockUser.email }); @@ -182,7 +182,7 @@ describe("handleSSOCallback", () => { locale: mockUser.locale, }); - await expect(handleSSOCallback({ user: mockUser, account: mockAccount })).rejects.toThrow( + await expect(handleSsoCallback({ user: mockUser, account: mockAccount })).rejects.toThrow( "Looks like you updated your email somewhere else. A user with this new email exists already." ); }); @@ -196,7 +196,7 @@ describe("handleSSOCallback", () => { locale: mockUser.locale, }); - const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); expect(result).toBe(true); }); @@ -208,7 +208,7 @@ describe("handleSSOCallback", () => { vi.mocked(getUserByEmail).mockResolvedValue(null); vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); - const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith({ @@ -228,7 +228,7 @@ describe("handleSSOCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); vi.mocked(getOrganization).mockResolvedValue(null); - const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); expect(result).toBe(true); expect(createOrganization).toHaveBeenCalledWith({ @@ -255,7 +255,7 @@ describe("handleSSOCallback", () => { vi.mocked(getUserByEmail).mockResolvedValue(null); vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); - const result = await handleSSOCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); expect(result).toBe(true); expect(createOrganization).not.toHaveBeenCalled(); @@ -276,7 +276,7 @@ describe("handleSSOCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser("Direct Name")); - const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith( @@ -297,7 +297,7 @@ describe("handleSSOCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser("John Doe")); - const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith( @@ -319,7 +319,7 @@ describe("handleSSOCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser("preferred.user")); - const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith( @@ -342,7 +342,7 @@ describe("handleSSOCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser("test.user")); - const result = await handleSSOCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); expect(result).toBe(true); diff --git a/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts b/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts index 5678a058b3..b092a570a4 100644 --- a/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts +++ b/apps/web/modules/organization/settings/teams/tests/__mocks__/actions.mock.ts @@ -40,6 +40,7 @@ export const mockUser: TUser = { locale: "en-US", imageUrl: null, role: null, + lastLoginAt: new Date(), isActive: true, }; diff --git a/apps/web/modules/survey/components/template-list/lib/user.ts b/apps/web/modules/survey/components/template-list/lib/user.ts index 7eac3c2eb5..e424c96a6f 100644 --- a/apps/web/modules/survey/components/template-list/lib/user.ts +++ b/apps/web/modules/survey/components/template-list/lib/user.ts @@ -27,6 +27,7 @@ export const updateUser = async (personId: string, data: TUserUpdateInput): Prom objective: true, notificationSettings: true, locale: true, + lastLoginAt: true, isActive: true, }, }); diff --git a/packages/database/migration/20250403042737_add_last_login_at_to_user/migration.sql b/packages/database/migration/20250403042737_add_last_login_at_to_user/migration.sql new file mode 100644 index 0000000000..2667ab2785 --- /dev/null +++ b/packages/database/migration/20250403042737_add_last_login_at_to_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "lastLoginAt" TIMESTAMP(3); diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index a759ce3998..a879c04e36 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -872,6 +872,7 @@ model User { locale String @default("en-US") surveys Survey[] teamUsers TeamUser[] + lastLoginAt DateTime? isActive Boolean @default(true) @@index([email]) diff --git a/packages/lib/survey/tests/__mock__/survey.mock.ts b/packages/lib/survey/tests/__mock__/survey.mock.ts index 8754c7fba3..825ef6c540 100644 --- a/packages/lib/survey/tests/__mock__/survey.mock.ts +++ b/packages/lib/survey/tests/__mock__/survey.mock.ts @@ -135,6 +135,7 @@ export const mockUser: TUser = { }, role: "other", locale: "en-US", + lastLoginAt: new Date(), isActive: true, }; diff --git a/packages/lib/user/service.ts b/packages/lib/user/service.ts index 00901f7738..b6350c3640 100644 --- a/packages/lib/user/service.ts +++ b/packages/lib/user/service.ts @@ -26,6 +26,7 @@ const responseSelection = { objective: true, notificationSettings: true, locale: true, + lastLoginAt: true, isActive: true, }; diff --git a/packages/types/user.ts b/packages/types/user.ts index 18f41bb52a..889040268d 100644 --- a/packages/types/user.ts +++ b/packages/types/user.ts @@ -58,6 +58,7 @@ export const ZUser = z.object({ objective: ZUserObjective.nullable(), notificationSettings: ZUserNotificationSettings, locale: ZUserLocale, + lastLoginAt: z.date().nullable(), isActive: z.boolean().default(true), }); @@ -73,6 +74,7 @@ export const ZUserUpdateInput = z.object({ imageUrl: z.string().nullish(), notificationSettings: ZUserNotificationSettings.optional(), locale: ZUserLocale.optional(), + lastLoginAt: z.date().nullish(), isActive: z.boolean().optional(), }); From c03e60ac0bb205d5be150e68bcfec6c7879d2795 Mon Sep 17 00:00:00 2001 From: victorvhs017 <115753265+victorvhs017@users.noreply.github.com> Date: Sat, 5 Apr 2025 08:54:21 -0300 Subject: [PATCH 146/411] feat: organization endpoints (#5076) Co-authored-by: Dhruwang Co-authored-by: pandeymangg --- apps/web/app/api/v1/auth.test.ts | 41 +- apps/web/app/api/v1/auth.ts | 4 + apps/web/app/api/v2/management/roles/route.ts | 3 - apps/web/app/api/v2/me/route.ts | 3 + .../[organizationId]/project-teams/route.ts | 3 + .../[organizationId]/teams/[teamId]/route.ts | 3 + .../[organizationId]/teams/route.ts | 3 + apps/web/app/api/v2/roles/route.ts | 3 + .../v2/{management => }/auth/api-wrapper.ts | 0 .../auth/authenticate-request.ts | 5 + .../auth/authenticated-api-client.ts | 0 .../auth/tests/api-wrapper.test.ts | 4 +- .../auth/tests/authenticate-request.test.ts | 30 +- .../tests/authenticated-api-client.test.ts | 0 apps/web/modules/api/v2/lib/response.ts | 3 + .../modules/api/v2/lib/tests/response.test.ts | 4 +- apps/web/modules/api/v2/lib/utils.ts | 2 +- .../modules/api/v2/management/lib/utils.ts | 2 +- .../responses/[responseId]/route.ts | 2 +- .../v2/management/responses/lib/openapi.ts | 3 +- .../api/v2/management/responses/route.ts | 2 +- .../api/v2/management/roles/lib/roles.ts | 26 - .../management/roles/lib/tests/roles.test.ts | 45 - .../contacts/[contactId]/route.ts | 2 +- .../webhooks/[webhookId]/lib/openapi.ts | 6 +- .../management/webhooks/[webhookId]/route.ts | 2 +- .../api/v2/management/webhooks/lib/openapi.ts | 5 +- .../api/v2/management/webhooks/route.ts | 2 +- apps/web/modules/api/v2/me/lib/openapi.ts | 26 + apps/web/modules/api/v2/me/route.ts | 23 + apps/web/modules/api/v2/me/types/me.ts | 0 apps/web/modules/api/v2/openapi-document.ts | 35 +- .../[organizationId]/lib/utils.test.ts | 55 + .../[organizationId]/lib/utils.ts | 27 + .../project-teams/lib/openapi.ts | 131 ++ .../project-teams/lib/project-teams.ts | 130 ++ .../lib/tests/project-teams.test.ts | 134 ++ .../project-teams/lib/utils.ts | 113 ++ .../[organizationId]/project-teams/route.ts | 170 +++ .../project-teams/types/project-teams.ts | 37 + .../teams/[teamId]/lib/openapi.ts | 85 ++ .../teams/[teamId]/lib/teams.ts | 141 ++ .../teams/[teamId]/lib/tests/teams.test.ts | 166 ++ .../[organizationId]/teams/[teamId]/route.ts | 100 ++ .../teams/[teamId]/types/teams.ts | 24 + .../[organizationId]/teams/lib/openapi.ts | 83 + .../[organizationId]/teams/lib/teams.ts | 71 + .../teams/lib/tests/teams.test.ts | 93 ++ .../teams/lib/tests/utils.test.ts | 43 + .../[organizationId]/teams/lib/utils.ts | 21 + .../[organizationId]/teams/route.ts | 64 + .../[organizationId]/teams/types/teams.ts | 23 + .../[organizationId]/types/organizations.ts | 16 + .../api/v2/organizations/lib/openapi.ts | 6 + .../v2/{management => }/roles/lib/openapi.ts | 7 +- apps/web/modules/api/v2/roles/lib/utils.ts | 21 + .../api/v2/{management => }/roles/route.ts | 6 +- .../api/v2/management/contacts/bulk/route.ts | 2 +- .../api-keys/components/add-api-key-modal.tsx | 1 + .../settings/api-keys/lib/api-key.ts | 61 +- apps/web/playwright/api/constants.ts | 7 +- .../api/organization/project-team.spec.ts | 120 ++ .../playwright/api/organization/team.spec.ts | 108 ++ .../api/{management => }/role.spec.ts | 4 +- apps/web/playwright/lib/utils.ts | 2 + docs/api-v2-reference/openapi.yml | 1349 ++++++++++++++--- packages/database/zod/api-keys.ts | 22 +- packages/database/zod/project-teams.ts | 30 + packages/database/zod/roles.ts | 7 + packages/database/zod/teams.ts | 31 + packages/types/api-key.ts | 4 +- packages/types/auth.ts | 11 +- pnpm-lock.yaml | 60 +- sonar-project.properties | 4 +- 74 files changed, 3492 insertions(+), 390 deletions(-) delete mode 100644 apps/web/app/api/v2/management/roles/route.ts create mode 100644 apps/web/app/api/v2/me/route.ts create mode 100644 apps/web/app/api/v2/organizations/[organizationId]/project-teams/route.ts create mode 100644 apps/web/app/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts create mode 100644 apps/web/app/api/v2/organizations/[organizationId]/teams/route.ts create mode 100644 apps/web/app/api/v2/roles/route.ts rename apps/web/modules/api/v2/{management => }/auth/api-wrapper.ts (100%) rename apps/web/modules/api/v2/{management => }/auth/authenticate-request.ts (85%) rename apps/web/modules/api/v2/{management => }/auth/authenticated-api-client.ts (100%) rename apps/web/modules/api/v2/{management => }/auth/tests/api-wrapper.test.ts (98%) rename apps/web/modules/api/v2/{management => }/auth/tests/authenticate-request.test.ts (75%) rename apps/web/modules/api/v2/{management => }/auth/tests/authenticated-api-client.test.ts (100%) delete mode 100644 apps/web/modules/api/v2/management/roles/lib/roles.ts delete mode 100644 apps/web/modules/api/v2/management/roles/lib/tests/roles.test.ts create mode 100644 apps/web/modules/api/v2/me/lib/openapi.ts create mode 100644 apps/web/modules/api/v2/me/route.ts create mode 100644 apps/web/modules/api/v2/me/types/me.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/openapi.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/utils.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/teams/types/teams.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/types/organizations.ts create mode 100644 apps/web/modules/api/v2/organizations/lib/openapi.ts rename apps/web/modules/api/v2/{management => }/roles/lib/openapi.ts (72%) create mode 100644 apps/web/modules/api/v2/roles/lib/utils.ts rename apps/web/modules/api/v2/{management => }/roles/route.ts (67%) create mode 100644 apps/web/playwright/api/organization/project-team.spec.ts create mode 100644 apps/web/playwright/api/organization/team.spec.ts rename apps/web/playwright/api/{management => }/role.spec.ts (89%) create mode 100644 packages/database/zod/project-teams.ts create mode 100644 packages/database/zod/roles.ts create mode 100644 packages/database/zod/teams.ts diff --git a/apps/web/app/api/v1/auth.test.ts b/apps/web/app/api/v1/auth.test.ts index b8ed5ccc4b..6659e5583a 100644 --- a/apps/web/app/api/v1/auth.test.ts +++ b/apps/web/app/api/v1/auth.test.ts @@ -62,9 +62,27 @@ describe("getApiKeyWithPermissions", () => { describe("hasPermission", () => { const permissions: TAPIKeyEnvironmentPermission[] = [ - { environmentId: "env-1", permission: "manage" }, - { environmentId: "env-2", permission: "write" }, - { environmentId: "env-3", permission: "read" }, + { + environmentId: "env-1", + permission: "manage", + environmentType: "development", + projectId: "project-1", + projectName: "Project 1", + }, + { + environmentId: "env-2", + permission: "write", + environmentType: "production", + projectId: "project-2", + projectName: "Project 2", + }, + { + environmentId: "env-3", + permission: "read", + environmentType: "development", + projectId: "project-3", + projectName: "Project 3", + }, ]; it("should return true for manage permission with any method", () => { @@ -108,7 +126,12 @@ describe("authenticateRequest", () => { { environmentId: "env-1", permission: "manage" as const, - environment: { id: "env-1" }, + environment: { + id: "env-1", + projectId: "project-1", + project: { name: "Project 1" }, + type: "development", + }, }, ], }; @@ -121,7 +144,15 @@ describe("authenticateRequest", () => { expect(result).toEqual({ type: "apiKey", - environmentPermissions: [{ environmentId: "env-1", permission: "manage" }], + environmentPermissions: [ + { + environmentId: "env-1", + permission: "manage", + environmentType: "development", + projectId: "project-1", + projectName: "Project 1", + }, + ], hashedApiKey: "hashed-key", apiKeyId: "api-key-id", organizationId: "org-id", diff --git a/apps/web/app/api/v1/auth.ts b/apps/web/app/api/v1/auth.ts index 44cd415c69..449f22355c 100644 --- a/apps/web/app/api/v1/auth.ts +++ b/apps/web/app/api/v1/auth.ts @@ -21,11 +21,15 @@ export const authenticateRequest = async (request: Request): Promise ({ environmentId: env.environmentId, + environmentType: env.environment.type, permission: env.permission, + projectId: env.environment.projectId, + projectName: env.environment.project.name, })), hashedApiKey, apiKeyId: apiKeyData.id, organizationId: apiKeyData.organizationId, + organizationAccess: apiKeyData.organizationAccess, }; return authentication; diff --git a/apps/web/app/api/v2/management/roles/route.ts b/apps/web/app/api/v2/management/roles/route.ts deleted file mode 100644 index 9580752584..0000000000 --- a/apps/web/app/api/v2/management/roles/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { GET } from "@/modules/api/v2/management/roles/route"; - -export { GET }; diff --git a/apps/web/app/api/v2/me/route.ts b/apps/web/app/api/v2/me/route.ts new file mode 100644 index 0000000000..a9fef632c5 --- /dev/null +++ b/apps/web/app/api/v2/me/route.ts @@ -0,0 +1,3 @@ +import { GET } from "@/modules/api/v2/me/route"; + +export { GET }; diff --git a/apps/web/app/api/v2/organizations/[organizationId]/project-teams/route.ts b/apps/web/app/api/v2/organizations/[organizationId]/project-teams/route.ts new file mode 100644 index 0000000000..b5c025f89b --- /dev/null +++ b/apps/web/app/api/v2/organizations/[organizationId]/project-teams/route.ts @@ -0,0 +1,3 @@ +import { DELETE, GET, POST, PUT } from "@/modules/api/v2/organizations/[organizationId]/project-teams/route"; + +export { GET, POST, PUT, DELETE }; diff --git a/apps/web/app/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts b/apps/web/app/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts new file mode 100644 index 0000000000..f5203a194c --- /dev/null +++ b/apps/web/app/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts @@ -0,0 +1,3 @@ +import { DELETE, GET, PUT } from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/route"; + +export { GET, PUT, DELETE }; diff --git a/apps/web/app/api/v2/organizations/[organizationId]/teams/route.ts b/apps/web/app/api/v2/organizations/[organizationId]/teams/route.ts new file mode 100644 index 0000000000..a3f7938e9e --- /dev/null +++ b/apps/web/app/api/v2/organizations/[organizationId]/teams/route.ts @@ -0,0 +1,3 @@ +import { GET, POST } from "@/modules/api/v2/organizations/[organizationId]/teams/route"; + +export { GET, POST }; diff --git a/apps/web/app/api/v2/roles/route.ts b/apps/web/app/api/v2/roles/route.ts new file mode 100644 index 0000000000..09811abca5 --- /dev/null +++ b/apps/web/app/api/v2/roles/route.ts @@ -0,0 +1,3 @@ +import { GET } from "@/modules/api/v2/roles/route"; + +export { GET }; diff --git a/apps/web/modules/api/v2/management/auth/api-wrapper.ts b/apps/web/modules/api/v2/auth/api-wrapper.ts similarity index 100% rename from apps/web/modules/api/v2/management/auth/api-wrapper.ts rename to apps/web/modules/api/v2/auth/api-wrapper.ts diff --git a/apps/web/modules/api/v2/management/auth/authenticate-request.ts b/apps/web/modules/api/v2/auth/authenticate-request.ts similarity index 85% rename from apps/web/modules/api/v2/management/auth/authenticate-request.ts rename to apps/web/modules/api/v2/auth/authenticate-request.ts index 2ec737799e..1eba850cb9 100644 --- a/apps/web/modules/api/v2/management/auth/authenticate-request.ts +++ b/apps/web/modules/api/v2/auth/authenticate-request.ts @@ -11,6 +11,7 @@ export const authenticateRequest = async ( if (!apiKey) return err({ type: "unauthorized" }); const apiKeyData = await getApiKeyWithPermissions(apiKey); + if (!apiKeyData) return err({ type: "unauthorized" }); const hashedApiKey = hashApiKey(apiKey); @@ -19,11 +20,15 @@ export const authenticateRequest = async ( type: "apiKey", environmentPermissions: apiKeyData.apiKeyEnvironments.map((env) => ({ environmentId: env.environmentId, + environmentType: env.environment.type, permission: env.permission, + projectId: env.environment.projectId, + projectName: env.environment.project.name, })), hashedApiKey, apiKeyId: apiKeyData.id, organizationId: apiKeyData.organizationId, + organizationAccess: apiKeyData.organizationAccess, }; return ok(authentication); }; diff --git a/apps/web/modules/api/v2/management/auth/authenticated-api-client.ts b/apps/web/modules/api/v2/auth/authenticated-api-client.ts similarity index 100% rename from apps/web/modules/api/v2/management/auth/authenticated-api-client.ts rename to apps/web/modules/api/v2/auth/authenticated-api-client.ts diff --git a/apps/web/modules/api/v2/management/auth/tests/api-wrapper.test.ts b/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts similarity index 98% rename from apps/web/modules/api/v2/management/auth/tests/api-wrapper.test.ts rename to apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts index 33e2a17145..dba952054f 100644 --- a/apps/web/modules/api/v2/management/auth/tests/api-wrapper.test.ts +++ b/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts @@ -1,7 +1,7 @@ +import { apiWrapper } from "@/modules/api/v2/auth/api-wrapper"; +import { authenticateRequest } from "@/modules/api/v2/auth/authenticate-request"; import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { apiWrapper } from "@/modules/api/v2/management/auth/api-wrapper"; -import { authenticateRequest } from "@/modules/api/v2/management/auth/authenticate-request"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { describe, expect, it, vi } from "vitest"; import { z } from "zod"; diff --git a/apps/web/modules/api/v2/management/auth/tests/authenticate-request.test.ts b/apps/web/modules/api/v2/auth/tests/authenticate-request.test.ts similarity index 75% rename from apps/web/modules/api/v2/management/auth/tests/authenticate-request.test.ts rename to apps/web/modules/api/v2/auth/tests/authenticate-request.test.ts index 02f4622e6e..27f4f78cae 100644 --- a/apps/web/modules/api/v2/management/auth/tests/authenticate-request.test.ts +++ b/apps/web/modules/api/v2/auth/tests/authenticate-request.test.ts @@ -34,12 +34,22 @@ describe("authenticateRequest", () => { { environmentId: "env-id-1", permission: "manage", - environment: { id: "env-id-1" }, + environment: { + id: "env-id-1", + projectId: "project-id-1", + type: "development", + project: { name: "Project 1" }, + }, }, { environmentId: "env-id-2", permission: "read", - environment: { id: "env-id-2" }, + environment: { + id: "env-id-2", + projectId: "project-id-2", + type: "production", + project: { name: "Project 2" }, + }, }, ], }; @@ -55,8 +65,20 @@ describe("authenticateRequest", () => { expect(result.data).toEqual({ type: "apiKey", environmentPermissions: [ - { environmentId: "env-id-1", permission: "manage" }, - { environmentId: "env-id-2", permission: "read" }, + { + environmentId: "env-id-1", + permission: "manage", + environmentType: "development", + projectId: "project-id-1", + projectName: "Project 1", + }, + { + environmentId: "env-id-2", + permission: "read", + environmentType: "production", + projectId: "project-id-2", + projectName: "Project 2", + }, ], hashedApiKey: "hashed-api-key", apiKeyId: "api-key-id", diff --git a/apps/web/modules/api/v2/management/auth/tests/authenticated-api-client.test.ts b/apps/web/modules/api/v2/auth/tests/authenticated-api-client.test.ts similarity index 100% rename from apps/web/modules/api/v2/management/auth/tests/authenticated-api-client.test.ts rename to apps/web/modules/api/v2/auth/tests/authenticated-api-client.test.ts diff --git a/apps/web/modules/api/v2/lib/response.ts b/apps/web/modules/api/v2/lib/response.ts index e3dc4c03f9..ea4c51c92c 100644 --- a/apps/web/modules/api/v2/lib/response.ts +++ b/apps/web/modules/api/v2/lib/response.ts @@ -122,9 +122,11 @@ const notFoundResponse = ({ const conflictResponse = ({ cors = false, cache = "private, no-store", + details = [], }: { cors?: boolean; cache?: string; + details?: ApiErrorDetails; } = {}) => { const headers = { ...(cors && corsHeaders), @@ -136,6 +138,7 @@ const conflictResponse = ({ error: { code: 409, message: "Conflict", + details, }, }, { diff --git a/apps/web/modules/api/v2/lib/tests/response.test.ts b/apps/web/modules/api/v2/lib/tests/response.test.ts index d370bc08ce..c5e5d233d9 100644 --- a/apps/web/modules/api/v2/lib/tests/response.test.ts +++ b/apps/web/modules/api/v2/lib/tests/response.test.ts @@ -85,13 +85,15 @@ describe("API Responses", () => { describe("conflictResponse", () => { test("return a 409 response", async () => { - const res = responses.conflictResponse(); + const details = [{ field: "resource", issue: "already exists" }]; + const res = responses.conflictResponse({ details }); expect(res.status).toBe(409); const body = await res.json(); expect(body).toEqual({ error: { code: 409, message: "Conflict", + details, }, }); }); diff --git a/apps/web/modules/api/v2/lib/utils.ts b/apps/web/modules/api/v2/lib/utils.ts index 92c9dbe9cc..845e22a7b6 100644 --- a/apps/web/modules/api/v2/lib/utils.ts +++ b/apps/web/modules/api/v2/lib/utils.ts @@ -16,7 +16,7 @@ export const handleApiError = (request: Request, err: ApiErrorResponseV2): Respo case "not_found": return responses.notFoundResponse({ details: err.details }); case "conflict": - return responses.conflictResponse(); + return responses.conflictResponse({ details: err.details }); case "unprocessable_entity": return responses.unprocessableEntityResponse({ details: err.details }); case "too_many_requests": diff --git a/apps/web/modules/api/v2/management/lib/utils.ts b/apps/web/modules/api/v2/management/lib/utils.ts index 3e601de2cc..33d5eb5fe8 100644 --- a/apps/web/modules/api/v2/management/lib/utils.ts +++ b/apps/web/modules/api/v2/management/lib/utils.ts @@ -9,7 +9,7 @@ export function pickCommonFilter(params: T) { return { limit, skip, sortBy, order, startDate, endDate }; } -type HasFindMany = Prisma.WebhookFindManyArgs | Prisma.ResponseFindManyArgs; +type HasFindMany = Prisma.WebhookFindManyArgs | Prisma.ResponseFindManyArgs | Prisma.TeamFindManyArgs | Prisma.ProjectTeamFindManyArgs; export function buildCommonFilterQuery(query: T, params: TGetFilter): T { const { limit, skip, sortBy, order, startDate, endDate } = params || {}; diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts index 7d3f182c93..d9c6916e62 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts @@ -1,6 +1,6 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper"; import { deleteResponse, diff --git a/apps/web/modules/api/v2/management/responses/lib/openapi.ts b/apps/web/modules/api/v2/management/responses/lib/openapi.ts index e46da37627..eb0dba284b 100644 --- a/apps/web/modules/api/v2/management/responses/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/responses/lib/openapi.ts @@ -5,7 +5,6 @@ import { } from "@/modules/api/v2/management/responses/[responseId]/lib/openapi"; import { ZGetResponsesFilter, ZResponseInput } from "@/modules/api/v2/management/responses/types/responses"; import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/types/openapi-response"; -import { z } from "zod"; import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; import { ZResponse } from "@formbricks/database/zod/responses"; @@ -22,7 +21,7 @@ export const getResponsesEndpoint: ZodOpenApiOperationObject = { description: "Responses retrieved successfully.", content: { "application/json": { - schema: z.array(responseWithMetaSchema(makePartialSchema(ZResponse))), + schema: responseWithMetaSchema(makePartialSchema(ZResponse)), }, }, }, diff --git a/apps/web/modules/api/v2/management/responses/route.ts b/apps/web/modules/api/v2/management/responses/route.ts index d23611f104..3dadae5a75 100644 --- a/apps/web/modules/api/v2/management/responses/route.ts +++ b/apps/web/modules/api/v2/management/responses/route.ts @@ -1,6 +1,6 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper"; import { ZGetResponsesFilter, ZResponseInput } from "@/modules/api/v2/management/responses/types/responses"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; diff --git a/apps/web/modules/api/v2/management/roles/lib/roles.ts b/apps/web/modules/api/v2/management/roles/lib/roles.ts deleted file mode 100644 index 41c022410f..0000000000 --- a/apps/web/modules/api/v2/management/roles/lib/roles.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; -import { ApiResponse } from "@/modules/api/v2/types/api-success"; -import { prisma } from "@formbricks/database"; -import { Result, err, ok } from "@formbricks/types/error-handlers"; - -export const getRoles = async (): Promise, ApiErrorResponseV2>> => { - try { - // We use a raw query to get all the roles because we can't list enum options with prisma - const results = await prisma.$queryRaw<{ unnest: string }[]>` - SELECT unnest(enum_range(NULL::"OrganizationRole")); - `; - - if (!results) { - // We set internal_server_error because it's an enum and we should always have the roles - return err({ type: "internal_server_error", details: [{ field: "roles", issue: "not found" }] }); - } - - const roles = results.map((row) => row.unnest); - - return ok({ - data: roles, - }); - } catch (error) { - return err({ type: "internal_server_error", details: [{ field: "roles", issue: error.message }] }); - } -}; diff --git a/apps/web/modules/api/v2/management/roles/lib/tests/roles.test.ts b/apps/web/modules/api/v2/management/roles/lib/tests/roles.test.ts deleted file mode 100644 index c23324382e..0000000000 --- a/apps/web/modules/api/v2/management/roles/lib/tests/roles.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { prisma } from "@formbricks/database"; -import { getRoles } from "../roles"; - -// Mock prisma with a $queryRaw function -vi.mock("@formbricks/database", () => ({ - prisma: { - $queryRaw: vi.fn(), - }, -})); - -describe("getRoles", () => { - it("returns roles on success", async () => { - (prisma.$queryRaw as any).mockResolvedValueOnce([{ unnest: "ADMIN" }, { unnest: "MEMBER" }]); - - const result = await getRoles(); - expect(result.ok).toBe(true); - - if (result.ok) { - expect(result.data.data).toEqual(["ADMIN", "MEMBER"]); - } - }); - - it("returns error if no results are found", async () => { - (prisma.$queryRaw as any).mockResolvedValueOnce(null); - - const result = await getRoles(); - expect(result.ok).toBe(false); - - if (!result.ok) { - expect(result.error?.type).toBe("internal_server_error"); - } - }); - - it("returns error on exception", async () => { - vi.mocked(prisma.$queryRaw).mockRejectedValueOnce(new Error("Test DB error")); - - const result = await getRoles(); - expect(result.ok).toBe(false); - - if (!result.ok) { - expect(result.error.type).toBe("internal_server_error"); - } - }); -}); diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts index 0012d91e47..e22228079c 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts @@ -1,6 +1,6 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper"; import { getContact } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/contacts"; import { getResponse } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/response"; diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts index 6d0c6e2615..0c5d5cb3d2 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts @@ -11,7 +11,7 @@ export const getWebhookEndpoint: ZodOpenApiOperationObject = { description: "Gets a webhook from the database.", requestParams: { path: z.object({ - webhookId: webhookIdSchema, + id: webhookIdSchema, }), }, tags: ["Management API > Webhooks"], @@ -34,7 +34,7 @@ export const deleteWebhookEndpoint: ZodOpenApiOperationObject = { tags: ["Management API > Webhooks"], requestParams: { path: z.object({ - webhookId: webhookIdSchema, + id: webhookIdSchema, }), }, responses: { @@ -56,7 +56,7 @@ export const updateWebhookEndpoint: ZodOpenApiOperationObject = { tags: ["Management API > Webhooks"], requestParams: { path: z.object({ - webhookId: webhookIdSchema, + id: webhookIdSchema, }), }, requestBody: { diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts index b5da782044..70c810cdf1 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts @@ -1,6 +1,6 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; import { getEnvironmentIdFromSurveyIds } from "@/modules/api/v2/management/lib/helper"; import { deleteWebhook, diff --git a/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts b/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts index 92bac070d2..3530e8230c 100644 --- a/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts @@ -5,7 +5,6 @@ import { } from "@/modules/api/v2/management/webhooks/[webhookId]/lib/openapi"; import { ZGetWebhooksFilter, ZWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks"; import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/types/openapi-response"; -import { z } from "zod"; import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; import { ZWebhook } from "@formbricks/database/zod/webhooks"; @@ -22,7 +21,7 @@ export const getWebhooksEndpoint: ZodOpenApiOperationObject = { description: "Webhooks retrieved successfully.", content: { "application/json": { - schema: z.array(responseWithMetaSchema(makePartialSchema(ZWebhook))), + schema: responseWithMetaSchema(makePartialSchema(ZWebhook)), }, }, }, @@ -60,7 +59,7 @@ export const webhookPaths: ZodOpenApiPathsObject = { get: getWebhooksEndpoint, post: createWebhookEndpoint, }, - "/webhooks/{webhookId}": { + "/webhooks/{id}": { get: getWebhookEndpoint, put: updateWebhookEndpoint, delete: deleteWebhookEndpoint, diff --git a/apps/web/modules/api/v2/management/webhooks/route.ts b/apps/web/modules/api/v2/management/webhooks/route.ts index cc5c0f3719..b18ed34a80 100644 --- a/apps/web/modules/api/v2/management/webhooks/route.ts +++ b/apps/web/modules/api/v2/management/webhooks/route.ts @@ -1,6 +1,6 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; import { getEnvironmentIdFromSurveyIds } from "@/modules/api/v2/management/lib/helper"; import { createWebhook, getWebhooks } from "@/modules/api/v2/management/webhooks/lib/webhook"; import { ZGetWebhooksFilter, ZWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks"; diff --git a/apps/web/modules/api/v2/me/lib/openapi.ts b/apps/web/modules/api/v2/me/lib/openapi.ts new file mode 100644 index 0000000000..f562cdbe97 --- /dev/null +++ b/apps/web/modules/api/v2/me/lib/openapi.ts @@ -0,0 +1,26 @@ +import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; +import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; +import { ZApiKeyData } from "@formbricks/database/zod/api-keys"; + +export const getMeEndpoint: ZodOpenApiOperationObject = { + operationId: "me", + summary: "Me", + description: "Fetches the projects and organizations associated with the API key.", + tags: ["Me"], + responses: { + "200": { + description: "API key information retrieved successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZApiKeyData), + }, + }, + }, + }, +}; + +export const mePaths: ZodOpenApiPathsObject = { + "/me": { + get: getMeEndpoint, + }, +}; diff --git a/apps/web/modules/api/v2/me/route.ts b/apps/web/modules/api/v2/me/route.ts new file mode 100644 index 0000000000..93878ea6dd --- /dev/null +++ b/apps/web/modules/api/v2/me/route.ts @@ -0,0 +1,23 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; +import { responses } from "@/modules/api/v2/lib/response"; +import { NextRequest } from "next/server"; + +export const GET = async (request: NextRequest) => + authenticatedApiClient({ + request, + handler: async ({ authentication }) => { + return responses.successResponse({ + data: { + environmentPermissions: authentication.environmentPermissions.map((permission) => ({ + environmentId: permission.environmentId, + environmentType: permission.environmentType, + permissions: permission.permission, + projectId: permission.projectId, + projectName: permission.projectName, + })), + organizationId: authentication.organizationId, + organizationAccess: authentication.organizationAccess, + }, + }); + }, + }); diff --git a/apps/web/modules/api/v2/me/types/me.ts b/apps/web/modules/api/v2/me/types/me.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/web/modules/api/v2/openapi-document.ts b/apps/web/modules/api/v2/openapi-document.ts index da89199cb7..7e8d805e35 100644 --- a/apps/web/modules/api/v2/openapi-document.ts +++ b/apps/web/modules/api/v2/openapi-document.ts @@ -2,18 +2,25 @@ import { contactAttributeKeyPaths } from "@/modules/api/v2/management/contact-at import { contactAttributePaths } from "@/modules/api/v2/management/contact-attributes/lib/openapi"; import { contactPaths } from "@/modules/api/v2/management/contacts/lib/openapi"; import { responsePaths } from "@/modules/api/v2/management/responses/lib/openapi"; -import { rolePaths } from "@/modules/api/v2/management/roles/lib/openapi"; import { surveyPaths } from "@/modules/api/v2/management/surveys/lib/openapi"; import { webhookPaths } from "@/modules/api/v2/management/webhooks/lib/openapi"; +import { mePaths } from "@/modules/api/v2/me/lib/openapi"; +import { projectTeamPaths } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi"; +import { teamPaths } from "@/modules/api/v2/organizations/[organizationId]/teams/lib/openapi"; +import { rolePaths } from "@/modules/api/v2/roles/lib/openapi"; import { bulkContactPaths } from "@/modules/ee/contacts/api/v2/management/contacts/bulk/lib/openapi"; import * as yaml from "yaml"; import { z } from "zod"; import { createDocument, extendZodWithOpenApi } from "zod-openapi"; +import { ZApiKeyData } from "@formbricks/database/zod/api-keys"; import { ZContact } from "@formbricks/database/zod/contact"; import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys"; import { ZContactAttribute } from "@formbricks/database/zod/contact-attributes"; +import { ZProjectTeam } from "@formbricks/database/zod/project-teams"; import { ZResponse } from "@formbricks/database/zod/responses"; +import { ZRoles } from "@formbricks/database/zod/roles"; import { ZSurveyWithoutQuestionType } from "@formbricks/database/zod/surveys"; +import { ZTeam } from "@formbricks/database/zod/teams"; import { ZWebhook } from "@formbricks/database/zod/webhooks"; extendZodWithOpenApi(z); @@ -26,6 +33,8 @@ const document = createDocument({ version: "2.0.0", }, paths: { + ...rolePaths, + ...mePaths, ...responsePaths, ...bulkContactPaths, ...contactPaths, @@ -33,7 +42,8 @@ const document = createDocument({ ...contactAttributeKeyPaths, ...surveyPaths, ...webhookPaths, - ...rolePaths, + ...teamPaths, + ...projectTeamPaths, }, servers: [ { @@ -42,6 +52,14 @@ const document = createDocument({ }, ], tags: [ + { + name: "Roles", + description: "Operations for managing roles.", + }, + { + name: "Me", + description: "Operations for managing your API key.", + }, { name: "Management API > Responses", description: "Operations for managing responses.", @@ -67,8 +85,12 @@ const document = createDocument({ description: "Operations for managing webhooks.", }, { - name: "Management API > Roles", - description: "Operations for managing roles.", + name: "Organizations API > Teams", + description: "Operations for managing teams.", + }, + { + name: "Organizations API > Project Teams", + description: "Operations for managing project teams.", }, ], components: { @@ -81,13 +103,16 @@ const document = createDocument({ }, }, schemas: { + role: ZRoles, + me: ZApiKeyData, response: ZResponse, contact: ZContact, contactAttribute: ZContactAttribute, contactAttributeKey: ZContactAttributeKey, survey: ZSurveyWithoutQuestionType, webhook: ZWebhook, - role: z.array(z.string()), + team: ZTeam, + projectTeam: ZProjectTeam, }, }, security: [ diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts new file mode 100644 index 0000000000..5a8167b6d9 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { logger } from "@formbricks/logger"; +import { OrganizationAccessType } from "@formbricks/types/api-key"; +import { hasOrganizationIdAndAccess } from "./utils"; + +describe("hasOrganizationIdAndAccess", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("should return false and log error if authentication has no organizationId", () => { + const spyError = vi.spyOn(logger, "error").mockImplementation(() => {}); + const authentication = { + organizationAccess: { accessControl: { read: true } }, + } as any; + + const result = hasOrganizationIdAndAccess("org1", authentication, "read" as OrganizationAccessType); + expect(result).toBe(false); + expect(spyError).toHaveBeenCalledWith("Organization ID is missing from the authentication object"); + }); + + it("should return false and log error if param organizationId does not match authentication organizationId", () => { + const spyError = vi.spyOn(logger, "error").mockImplementation(() => {}); + const authentication = { + organizationId: "org2", + organizationAccess: { accessControl: { read: true } }, + } as any; + + const result = hasOrganizationIdAndAccess("org1", authentication, "read" as OrganizationAccessType); + expect(result).toBe(false); + expect(spyError).toHaveBeenCalledWith( + "Organization ID from params does not match the authenticated organization ID" + ); + }); + + it("should return false if access type is missing in organizationAccess", () => { + const authentication = { + organizationId: "org1", + organizationAccess: { accessControl: {} }, + } as any; + + const result = hasOrganizationIdAndAccess("org1", authentication, "read" as OrganizationAccessType); + expect(result).toBe(false); + }); + + it("should return true if organizationId and access type are valid", () => { + const authentication = { + organizationId: "org1", + organizationAccess: { accessControl: { read: true } }, + } as any; + + const result = hasOrganizationIdAndAccess("org1", authentication, "read" as OrganizationAccessType); + expect(result).toBe(true); + }); +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts new file mode 100644 index 0000000000..59d1016080 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts @@ -0,0 +1,27 @@ +import { logger } from "@formbricks/logger"; +import { OrganizationAccessType } from "@formbricks/types/api-key"; +import { TAuthenticationApiKey } from "@formbricks/types/auth"; + +export const hasOrganizationIdAndAccess = ( + paramOrganizationId: string, + authentication: TAuthenticationApiKey, + accessType: OrganizationAccessType +): boolean => { + if (!authentication.organizationId) { + logger.error("Organization ID is missing from the authentication object"); + + return false; + } + + if (paramOrganizationId !== authentication.organizationId) { + logger.error("Organization ID from params does not match the authenticated organization ID"); + + return false; + } + + if (!authentication.organizationAccess?.accessControl?.[accessType]) { + return false; + } + + return true; +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts new file mode 100644 index 0000000000..8a8dc57353 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts @@ -0,0 +1,131 @@ +import { + ZGetProjectTeamUpdateFilter, + ZGetProjectTeamsFilter, + ZProjectTeamInput, + projectTeamUpdateSchema, +} from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { organizationServer } from "@/modules/api/v2/organizations/lib/openapi"; +import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/types/openapi-response"; +import { z } from "zod"; +import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; +import { ZProjectTeam } from "@formbricks/database/zod/project-teams"; + +export const getProjectTeamsEndpoint: ZodOpenApiOperationObject = { + operationId: "getProjectTeams", + summary: "Get project teams", + description: "Gets projectTeams from the database.", + requestParams: { + query: ZGetProjectTeamsFilter.sourceType().required(), + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + }, + tags: ["Organizations API > Project Teams"], + responses: { + "200": { + description: "Project teams retrieved successfully.", + content: { + "application/json": { + schema: responseWithMetaSchema(makePartialSchema(ZProjectTeam)), + }, + }, + }, + }, +}; + +export const createProjectTeamEndpoint: ZodOpenApiOperationObject = { + operationId: "createProjectTeam", + summary: "Create a projectTeam", + description: "Creates a project team in the database.", + requestParams: { + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + }, + tags: ["Organizations API > Project Teams"], + requestBody: { + required: true, + description: "The project team to create", + content: { + "application/json": { + schema: ZProjectTeamInput, + }, + }, + }, + responses: { + "201": { + description: "Project team created successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZProjectTeam), + }, + }, + }, + }, +}; + +export const deleteProjectTeamEndpoint: ZodOpenApiOperationObject = { + operationId: "deleteProjectTeam", + summary: "Delete a project team", + description: "Deletes a project team from the database.", + tags: ["Organizations API > Project Teams"], + requestParams: { + query: ZGetProjectTeamUpdateFilter.required(), + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + }, + responses: { + "200": { + description: "Project team deleted successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZProjectTeam), + }, + }, + }, + }, +}; + +export const updateProjectTeamEndpoint: ZodOpenApiOperationObject = { + operationId: "updateProjectTeam", + summary: "Update a project team", + description: "Updates a project team in the database.", + tags: ["Organizations API > Project Teams"], + requestParams: { + query: ZGetProjectTeamUpdateFilter.required(), + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + }, + requestBody: { + required: true, + description: "The project team to update", + content: { + "application/json": { + schema: projectTeamUpdateSchema, + }, + }, + }, + responses: { + "200": { + description: "Project team updated successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZProjectTeam), + }, + }, + }, + }, +}; + +export const projectTeamPaths: ZodOpenApiPathsObject = { + "/{organizationId}/project-teams": { + servers: organizationServer, + get: getProjectTeamsEndpoint, + post: createProjectTeamEndpoint, + put: updateProjectTeamEndpoint, + delete: deleteProjectTeamEndpoint, + }, +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts new file mode 100644 index 0000000000..1a2bcee222 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts @@ -0,0 +1,130 @@ +import { teamCache } from "@/lib/cache/team"; +import { getProjectTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils"; +import { + TGetProjectTeamsFilter, + TProjectTeamInput, + projectTeamUpdateSchema, +} from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; +import { ProjectTeam } from "@prisma/client"; +import { z } from "zod"; +import { prisma } from "@formbricks/database"; +import { projectCache } from "@formbricks/lib/project/cache"; +import { captureTelemetry } from "@formbricks/lib/telemetry"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getProjectTeams = async ( + organizationId: string, + params: TGetProjectTeamsFilter +): Promise, ApiErrorResponseV2>> => { + try { + const [projectTeams, count] = await prisma.$transaction([ + prisma.projectTeam.findMany({ + ...getProjectTeamsQuery(organizationId, params), + }), + prisma.projectTeam.count({ + where: getProjectTeamsQuery(organizationId, params).where, + }), + ]); + + return ok({ + data: projectTeams, + meta: { + total: count, + limit: params.limit, + offset: params.skip, + }, + }); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "projectTeam", issue: error.message }] }); + } +}; + +export const createProjectTeam = async ( + teamInput: TProjectTeamInput +): Promise> => { + captureTelemetry("project team created"); + + const { teamId, projectId, permission } = teamInput; + + try { + const projectTeam = await prisma.projectTeam.create({ + data: { + teamId, + projectId, + permission, + }, + }); + + projectCache.revalidate({ + id: projectId, + }); + + teamCache.revalidate({ + id: teamId, + }); + + return ok(projectTeam); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "projecteam", issue: error.message }] }); + } +}; + +export const updateProjectTeam = async ( + teamId: string, + projectId: string, + teamInput: z.infer +): Promise> => { + try { + const updatedProjectTeam = await prisma.projectTeam.update({ + where: { + projectId_teamId: { + projectId, + teamId, + }, + }, + data: teamInput, + }); + + projectCache.revalidate({ + id: projectId, + }); + + teamCache.revalidate({ + id: teamId, + }); + + return ok(updatedProjectTeam); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "projecteam", issue: error.message }] }); + } +}; + +export const deleteProjectTeam = async ( + teamId: string, + projectId: string +): Promise> => { + try { + const deletedProjectTeam = await prisma.projectTeam.delete({ + where: { + projectId_teamId: { + projectId, + teamId, + }, + }, + }); + + projectCache.revalidate({ + id: projectId, + }); + + teamCache.revalidate({ + id: teamId, + }); + + return ok(deletedProjectTeam); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "projecteam", issue: error.message }] }); + } +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts new file mode 100644 index 0000000000..3ced4cf4ba --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts @@ -0,0 +1,134 @@ +import { + TGetProjectTeamsFilter, + TProjectTeamInput, + projectTeamUpdateSchema, +} from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { TypeOf } from "zod"; +import { prisma } from "@formbricks/database"; +import { createProjectTeam, deleteProjectTeam, getProjectTeams, updateProjectTeam } from "../project-teams"; + +vi.mock("@formbricks/database", () => ({ + prisma: { + projectTeam: { + findMany: vi.fn(), + count: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, + $transaction: vi.fn(), + }, +})); + +describe("ProjectTeams Lib", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getProjectTeams", () => { + it("returns projectTeams with meta on success", async () => { + const mockTeams = [{ id: "projTeam1", organizationId: "orgx", projectId: "p1", teamId: "t1" }]; + (prisma.$transaction as any).mockResolvedValueOnce([mockTeams, mockTeams.length]); + const result = await getProjectTeams("orgx", { skip: 0, limit: 10 } as TGetProjectTeamsFilter); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.data).toEqual(mockTeams); + expect(result.data.meta).not.toBeNull(); + if (result.data.meta) { + expect(result.data.meta.total).toBe(mockTeams.length); + } + } + }); + + it("returns internal_server_error on exception", async () => { + (prisma.$transaction as any).mockRejectedValueOnce(new Error("DB error")); + const result = await getProjectTeams("orgx", { skip: 0, limit: 10 } as TGetProjectTeamsFilter); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("createProjectTeam", () => { + it("creates a projectTeam successfully", async () => { + const mockCreated = { id: "ptx", projectId: "p1", teamId: "t1", organizationId: "orgx" }; + (prisma.projectTeam.create as any).mockResolvedValueOnce(mockCreated); + const result = await createProjectTeam({ + projectId: "p1", + teamId: "t1", + } as TProjectTeamInput); + expect(result.ok).toBe(true); + if (result.ok) { + expect((result.data as any).id).toBe("ptx"); + } + }); + + it("returns internal_server_error on error", async () => { + (prisma.projectTeam.create as any).mockRejectedValueOnce(new Error("Create error")); + const result = await createProjectTeam({ + projectId: "p1", + teamId: "t1", + } as TProjectTeamInput); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("updateProjectTeam", () => { + it("updates a projectTeam successfully", async () => { + (prisma.projectTeam.update as any).mockResolvedValueOnce({ + id: "pt01", + projectId: "p1", + teamId: "t1", + permission: "READ", + }); + const result = await updateProjectTeam("t1", "p1", { permission: "READ" } as unknown as TypeOf< + typeof projectTeamUpdateSchema + >); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.permission).toBe("READ"); + } + }); + + it("returns internal_server_error on error", async () => { + (prisma.projectTeam.update as any).mockRejectedValueOnce(new Error("Update error")); + const result = await updateProjectTeam("t1", "p1", { permission: "READ" } as unknown as TypeOf< + typeof projectTeamUpdateSchema + >); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("deleteProjectTeam", () => { + it("deletes a projectTeam successfully", async () => { + (prisma.projectTeam.delete as any).mockResolvedValueOnce({ + projectId: "p1", + teamId: "t1", + permission: "READ", + }); + const result = await deleteProjectTeam("t1", "p1"); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.projectId).toBe("p1"); + expect(result.data.teamId).toBe("t1"); + } + }); + + it("returns internal_server_error on error", async () => { + (prisma.projectTeam.delete as any).mockRejectedValueOnce(new Error("Delete error")); + const result = await deleteProjectTeam("t1", "p1"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts new file mode 100644 index 0000000000..a1cdbea501 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils.ts @@ -0,0 +1,113 @@ +import { teamCache } from "@/lib/cache/team"; +import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; +import { TGetProjectTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { organizationCache } from "@formbricks/lib/organization/cache"; +import { projectCache } from "@formbricks/lib/project/cache"; +import { TAuthenticationApiKey } from "@formbricks/types/auth"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getProjectTeamsQuery = (organizationId: string, params: TGetProjectTeamsFilter) => { + const { teamId, projectId } = params || {}; + + let query: Prisma.ProjectTeamFindManyArgs = { + where: { + team: { + organizationId, + }, + }, + }; + + if (teamId) { + query = { + ...query, + where: { + ...query.where, + teamId, + }, + }; + } + + if (projectId) { + query = { + ...query, + where: { + ...query.where, + projectId, + project: { + organizationId, + }, + }, + }; + } + + const baseFilter = pickCommonFilter(params); + + if (baseFilter) { + query = buildCommonFilterQuery(query, baseFilter); + } + + return query; +}; + +export const validateTeamIdAndProjectId = reactCache( + async (organizationId: string, teamId: string, projectId: string) => + cache( + async (): Promise> => { + try { + const hasAccess = await prisma.organization.findFirst({ + where: { + id: organizationId, + teams: { + some: { + id: teamId, + }, + }, + projects: { + some: { + id: projectId, + }, + }, + }, + }); + + if (!hasAccess) { + return err({ type: "not_found", details: [{ field: "teamId/projectId", issue: "not_found" }] }); + } + + return ok(true); + } catch (error) { + return err({ + type: "internal_server_error", + details: [{ field: "teamId/projectId", issue: error.message }], + }); + } + }, + [`validateTeamIdAndProjectId-${organizationId}-${teamId}-${projectId}`], + { + tags: [ + teamCache.tag.byId(teamId), + projectCache.tag.byId(projectId), + organizationCache.tag.byId(organizationId), + ], + } + )() +); + +export const checkAuthenticationAndAccess = async ( + teamId: string, + projectId: string, + authentication: TAuthenticationApiKey +): Promise> => { + const hasAccess = await validateTeamIdAndProjectId(authentication.organizationId, teamId, projectId); + + if (!hasAccess.ok) { + return err(hasAccess.error); + } + + return ok(true); +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts new file mode 100644 index 0000000000..07360dba80 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts @@ -0,0 +1,170 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; +import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; +import { hasOrganizationIdAndAccess } from "@/modules/api/v2/organizations/[organizationId]/lib/utils"; +import { checkAuthenticationAndAccess } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/utils"; +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { z } from "zod"; +import { OrganizationAccessType } from "@formbricks/types/api-key"; +import { + createProjectTeam, + deleteProjectTeam, + getProjectTeams, + updateProjectTeam, +} from "./lib/project-teams"; +import { + ZGetProjectTeamUpdateFilter, + ZGetProjectTeamsFilter, + ZProjectTeamInput, + projectTeamUpdateSchema, +} from "./types/project-teams"; + +export async function GET(request: Request, props: { params: Promise<{ organizationId: string }> }) { + return authenticatedApiClient({ + request, + schemas: { + query: ZGetProjectTeamsFilter.sourceType(), + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ parsedInput: { query, params }, authentication }) => { + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Read)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const result = await getProjectTeams(authentication.organizationId, query!); + + if (!result.ok) { + return handleApiError(request, result.error); + } + + return responses.successResponse(result.data); + }, + }); +} + +export async function POST(request: Request, props: { params: Promise<{ organizationId: string }> }) { + return authenticatedApiClient({ + request, + schemas: { + body: ZProjectTeamInput, + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ parsedInput: { body, params }, authentication }) => { + const { teamId, projectId } = body!; + + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const hasAccess = await checkAuthenticationAndAccess(teamId, projectId, authentication); + + if (!hasAccess.ok) { + return handleApiError(request, hasAccess.error); + } + + // check if project team already exists + const existingProjectTeam = await getProjectTeams(authentication.organizationId, { + teamId, + projectId, + limit: 10, + skip: 0, + sortBy: "createdAt", + order: "desc", + }); + + if (!existingProjectTeam.ok) { + return handleApiError(request, existingProjectTeam.error); + } + + if (existingProjectTeam.data.data.length > 0) { + return handleApiError(request, { + type: "conflict", + details: [{ field: "projectTeam", issue: "Project team already exists" }], + }); + } + const result = await createProjectTeam(body!); + if (!result.ok) { + return handleApiError(request, result.error); + } + + return responses.successResponse({ data: result.data, cors: true }); + }, + }); +} + +export async function PUT(request: Request, props: { params: Promise<{ organizationId: string }> }) { + return authenticatedApiClient({ + request, + schemas: { + body: projectTeamUpdateSchema, + query: ZGetProjectTeamUpdateFilter, + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ parsedInput: { query, body, params }, authentication }) => { + const { teamId, projectId } = query!; + + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const hasAccess = await checkAuthenticationAndAccess(teamId, projectId, authentication); + + if (!hasAccess.ok) { + return handleApiError(request, hasAccess.error); + } + + const result = await updateProjectTeam(teamId, projectId, body!); + if (!result.ok) { + return handleApiError(request, result.error); + } + + return responses.successResponse({ data: result.data, cors: true }); + }, + }); +} + +export async function DELETE(request: Request, props: { params: Promise<{ organizationId: string }> }) { + return authenticatedApiClient({ + request, + schemas: { + query: ZGetProjectTeamUpdateFilter, + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ parsedInput: { query, params }, authentication }) => { + const { teamId, projectId } = query!; + + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const hasAccess = await checkAuthenticationAndAccess(teamId, projectId, authentication); + + if (!hasAccess.ok) { + return handleApiError(request, hasAccess.error); + } + + const result = await deleteProjectTeam(teamId, projectId); + if (!result.ok) { + return handleApiError(request, result.error); + } + + return responses.successResponse({ data: result.data, cors: true }); + }, + }); +} diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts new file mode 100644 index 0000000000..11bf7c4580 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts @@ -0,0 +1,37 @@ +import { ZGetFilter } from "@/modules/api/v2/types/api-filter"; +import { z } from "zod"; +import { ZProjectTeam } from "@formbricks/database/zod/project-teams"; + +export const ZGetProjectTeamsFilter = ZGetFilter.extend({ + teamId: z.string().cuid2().optional(), + projectId: z.string().cuid2().optional(), +}).refine( + (data) => { + if (data.startDate && data.endDate && data.startDate > data.endDate) { + return false; + } + return true; + }, + { + message: "startDate must be before endDate", + } +); + +export type TGetProjectTeamsFilter = z.infer; + +export const ZProjectTeamInput = ZProjectTeam.pick({ + teamId: true, + projectId: true, + permission: true, +}); + +export type TProjectTeamInput = z.infer; + +export const ZGetProjectTeamUpdateFilter = z.object({ + teamId: z.string().cuid2(), + projectId: z.string().cuid2(), +}); + +export const projectTeamUpdateSchema = ZProjectTeam.pick({ + permission: true, +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/openapi.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/openapi.ts new file mode 100644 index 0000000000..18bd73ed56 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/openapi.ts @@ -0,0 +1,85 @@ +import { ZTeamIdSchema } from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams"; +import { ZTeamInput } from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams"; +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; +import { z } from "zod"; +import { ZodOpenApiOperationObject } from "zod-openapi"; +import { ZTeam } from "@formbricks/database/zod/teams"; + +export const getTeamEndpoint: ZodOpenApiOperationObject = { + operationId: "getTeam", + summary: "Get a team", + description: "Gets a team from the database.", + requestParams: { + path: z.object({ + id: ZTeamIdSchema, + organizationId: ZOrganizationIdSchema, + }), + }, + tags: ["Organizations API > Teams"], + responses: { + "200": { + description: "Team retrieved successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZTeam), + }, + }, + }, + }, +}; + +export const deleteTeamEndpoint: ZodOpenApiOperationObject = { + operationId: "deleteTeam", + summary: "Delete a team", + description: "Deletes a team from the database.", + tags: ["Organizations API > Teams"], + requestParams: { + path: z.object({ + id: ZTeamIdSchema, + organizationId: ZOrganizationIdSchema, + }), + }, + responses: { + "200": { + description: "Team deleted successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZTeam), + }, + }, + }, + }, +}; + +export const updateTeamEndpoint: ZodOpenApiOperationObject = { + operationId: "updateTeam", + summary: "Update a team", + description: "Updates a team in the database.", + tags: ["Organizations API > Teams"], + requestParams: { + path: z.object({ + id: ZTeamIdSchema, + organizationId: ZOrganizationIdSchema, + }), + }, + requestBody: { + required: true, + description: "The team to update", + content: { + "application/json": { + schema: ZTeamInput, + }, + }, + }, + responses: { + "200": { + description: "Team updated successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZTeam), + }, + }, + }, + }, +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts new file mode 100644 index 0000000000..90a9d43c8c --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams.ts @@ -0,0 +1,141 @@ +import { organizationCache } from "@/lib/cache/organization"; +import { teamCache } from "@/lib/cache/team"; +import { ZTeamUpdateSchema } from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams"; +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { Team } from "@prisma/client"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { cache as reactCache } from "react"; +import { z } from "zod"; +import { prisma } from "@formbricks/database"; +import { PrismaErrorType } from "@formbricks/database/types/error"; +import { cache } from "@formbricks/lib/cache"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getTeam = reactCache(async (organizationId: string, teamId: string) => + cache( + async (): Promise> => { + try { + const responsePrisma = await prisma.team.findUnique({ + where: { + id: teamId, + organizationId, + }, + }); + + if (!responsePrisma) { + return err({ type: "not_found", details: [{ field: "team", issue: "not found" }] }); + } + + return ok(responsePrisma); + } catch (error) { + return err({ + type: "internal_server_error", + details: [{ field: "team", issue: error.message }], + }); + } + }, + [`organizationId-${organizationId}-getTeam-${teamId}`], + { + tags: [teamCache.tag.byId(teamId), organizationCache.tag.byId(organizationId)], + } + )() +); + +export const deleteTeam = async ( + organizationId: string, + teamId: string +): Promise> => { + try { + const deletedTeam = await prisma.team.delete({ + where: { + id: teamId, + organizationId, + }, + include: { + projectTeams: { + select: { + projectId: true, + }, + }, + }, + }); + + teamCache.revalidate({ + id: deletedTeam.id, + organizationId: deletedTeam.organizationId, + }); + + for (const projectTeam of deletedTeam.projectTeams) { + teamCache.revalidate({ + projectId: projectTeam.projectId, + }); + } + + return ok(deletedTeam); + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + if ( + error.code === PrismaErrorType.RecordDoesNotExist || + error.code === PrismaErrorType.RelatedRecordDoesNotExist + ) { + return err({ + type: "not_found", + details: [{ field: "team", issue: "not found" }], + }); + } + } + + return err({ + type: "internal_server_error", + details: [{ field: "team", issue: error.message }], + }); + } +}; + +export const updateTeam = async ( + organizationId: string, + teamId: string, + teamInput: z.infer +): Promise> => { + try { + const updatedTeam = await prisma.team.update({ + where: { + id: teamId, + organizationId, + }, + data: teamInput, + include: { + projectTeams: { select: { projectId: true } }, + }, + }); + + teamCache.revalidate({ + id: updatedTeam.id, + organizationId: updatedTeam.organizationId, + }); + + for (const projectTeam of updatedTeam.projectTeams) { + teamCache.revalidate({ + projectId: projectTeam.projectId, + }); + } + + return ok(updatedTeam); + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + if ( + error.code === PrismaErrorType.RecordDoesNotExist || + error.code === PrismaErrorType.RelatedRecordDoesNotExist + ) { + return err({ + type: "not_found", + details: [{ field: "team", issue: "not found" }], + }); + } + } + return err({ + type: "internal_server_error", + details: [{ field: "team", issue: error.message }], + }); + } +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts new file mode 100644 index 0000000000..f7ae2215f6 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/tests/teams.test.ts @@ -0,0 +1,166 @@ +import { teamCache } from "@/lib/cache/team"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { PrismaErrorType } from "@formbricks/database/types/error"; +import { deleteTeam, getTeam, updateTeam } from "../teams"; + +vi.mock("@formbricks/database", () => ({ + prisma: { + team: { + findUnique: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, + }, +})); + +// Define a mock team +const mockTeam = { + id: "team123", + organizationId: "org456", + name: "Test Team", + projectTeams: [{ projectId: "proj1" }, { projectId: "proj2" }], +}; + +describe("Teams Lib", () => { + describe("getTeam", () => { + it("returns the team when found", async () => { + (prisma.team.findUnique as any).mockResolvedValueOnce(mockTeam); + const result = await getTeam("org456", "team123"); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data).toEqual(mockTeam); + } + expect(prisma.team.findUnique).toHaveBeenCalledWith({ + where: { id: "team123", organizationId: "org456" }, + }); + }); + + it("returns a not_found error when team is missing", async () => { + (prisma.team.findUnique as any).mockResolvedValueOnce(null); + const result = await getTeam("org456", "team123"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toEqual({ + type: "not_found", + details: [{ field: "team", issue: "not found" }], + }); + } + }); + + it("returns an internal_server_error when prisma throws", async () => { + (prisma.team.findUnique as any).mockRejectedValueOnce(new Error("DB error")); + const result = await getTeam("org456", "team123"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("deleteTeam", () => { + it("deletes the team and revalidates cache", async () => { + (prisma.team.delete as any).mockResolvedValueOnce(mockTeam); + // Mock teamCache.revalidate + const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {}); + const result = await deleteTeam("org456", "team123"); + expect(prisma.team.delete).toHaveBeenCalledWith({ + where: { id: "team123", organizationId: "org456" }, + include: { projectTeams: { select: { projectId: true } } }, + }); + expect(revalidateMock).toHaveBeenCalledWith({ + id: mockTeam.id, + organizationId: mockTeam.organizationId, + }); + for (const pt of mockTeam.projectTeams) { + expect(revalidateMock).toHaveBeenCalledWith({ projectId: pt.projectId }); + } + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data).toEqual(mockTeam); + } + }); + + it("returns not_found error on known prisma error", async () => { + (prisma.team.delete as any).mockRejectedValueOnce( + new PrismaClientKnownRequestError("Not found", { + code: PrismaErrorType.RecordDoesNotExist, + clientVersion: "1.0.0", + meta: {}, + }) + ); + const result = await deleteTeam("org456", "team123"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toEqual({ + type: "not_found", + details: [{ field: "team", issue: "not found" }], + }); + } + }); + + it("returns internal_server_error on exception", async () => { + (prisma.team.delete as any).mockRejectedValueOnce(new Error("Delete failed")); + const result = await deleteTeam("org456", "team123"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("updateTeam", () => { + const updateInput = { name: "Updated Team" }; + const updatedTeam = { ...mockTeam, ...updateInput }; + + it("updates the team successfully and revalidates cache", async () => { + (prisma.team.update as any).mockResolvedValueOnce(updatedTeam); + const revalidateMock = vi.spyOn(teamCache, "revalidate").mockImplementation(() => {}); + const result = await updateTeam("org456", "team123", updateInput); + expect(prisma.team.update).toHaveBeenCalledWith({ + where: { id: "team123", organizationId: "org456" }, + data: updateInput, + include: { projectTeams: { select: { projectId: true } } }, + }); + expect(revalidateMock).toHaveBeenCalledWith({ + id: updatedTeam.id, + organizationId: updatedTeam.organizationId, + }); + for (const pt of updatedTeam.projectTeams) { + expect(revalidateMock).toHaveBeenCalledWith({ projectId: pt.projectId }); + } + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data).toEqual(updatedTeam); + } + }); + + it("returns not_found error when update fails due to missing team", async () => { + (prisma.team.update as any).mockRejectedValueOnce( + new PrismaClientKnownRequestError("Not found", { + code: PrismaErrorType.RecordDoesNotExist, + clientVersion: "1.0.0", + meta: {}, + }) + ); + const result = await updateTeam("org456", "team123", updateInput); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toEqual({ + type: "not_found", + details: [{ field: "team", issue: "not found" }], + }); + } + }); + + it("returns internal_server_error on generic exception", async () => { + (prisma.team.update as any).mockRejectedValueOnce(new Error("Update failed")); + const result = await updateTeam("org456", "team123", updateInput); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts new file mode 100644 index 0000000000..2f12f6aec3 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/route.ts @@ -0,0 +1,100 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; +import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; +import { hasOrganizationIdAndAccess } from "@/modules/api/v2/organizations/[organizationId]/lib/utils"; +import { + deleteTeam, + getTeam, + updateTeam, +} from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/teams"; +import { + ZTeamIdSchema, + ZTeamUpdateSchema, +} from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams"; +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { z } from "zod"; +import { OrganizationAccessType } from "@formbricks/types/api-key"; + +export const GET = async ( + request: Request, + props: { params: Promise<{ teamId: string; organizationId: string }> } +) => + authenticatedApiClient({ + request, + schemas: { + params: z.object({ teamId: ZTeamIdSchema, organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput: { params } }) => { + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Read)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const team = await getTeam(params!.organizationId, params!.teamId); + if (!team.ok) { + return handleApiError(request, team.error); + } + + return responses.successResponse(team); + }, + }); + +export const DELETE = async ( + request: Request, + props: { params: Promise<{ teamId: string; organizationId: string }> } +) => + authenticatedApiClient({ + request, + schemas: { + params: z.object({ teamId: ZTeamIdSchema, organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput: { params } }) => { + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const team = await deleteTeam(params!.organizationId, params!.teamId); + + if (!team.ok) { + return handleApiError(request, team.error); + } + + return responses.successResponse(team); + }, + }); + +export const PUT = ( + request: Request, + props: { params: Promise<{ teamId: string; organizationId: string }> } +) => + authenticatedApiClient({ + request, + externalParams: props.params, + schemas: { + params: z.object({ teamId: ZTeamIdSchema, organizationId: ZOrganizationIdSchema }), + body: ZTeamUpdateSchema, + }, + handler: async ({ authentication, parsedInput: { body, params } }) => { + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const team = await updateTeam(params!.organizationId, params!.teamId, body!); + + if (!team.ok) { + return handleApiError(request, team.error); + } + + return responses.successResponse(team); + }, + }); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams.ts new file mode 100644 index 0000000000..10f6a16dc8 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { extendZodWithOpenApi } from "zod-openapi"; +import { ZTeam } from "@formbricks/database/zod/teams"; + +extendZodWithOpenApi(z); + +export const ZTeamIdSchema = z + .string() + .cuid2() + .openapi({ + ref: "teamId", + description: "The ID of the team", + param: { + name: "id", + in: "path", + }, + }); + +export const ZTeamUpdateSchema = ZTeam.omit({ + id: true, + createdAt: true, + updatedAt: true, + organizationId: true, +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts new file mode 100644 index 0000000000..443d71ea98 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts @@ -0,0 +1,83 @@ +import { + deleteTeamEndpoint, + getTeamEndpoint, + updateTeamEndpoint, +} from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/lib/openapi"; +import { + ZGetTeamsFilter, + ZTeamInput, +} from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams"; +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { organizationServer } from "@/modules/api/v2/organizations/lib/openapi"; +import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/types/openapi-response"; +import { z } from "zod"; +import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; +import { ZTeam } from "@formbricks/database/zod/teams"; + +export const getTeamsEndpoint: ZodOpenApiOperationObject = { + operationId: "getTeams", + summary: "Get teams", + description: "Gets teams from the database.", + requestParams: { + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + query: ZGetTeamsFilter.sourceType().required(), + }, + tags: ["Organizations API > Teams"], + responses: { + "200": { + description: "Teams retrieved successfully.", + content: { + "application/json": { + schema: responseWithMetaSchema(makePartialSchema(ZTeam)), + }, + }, + }, + }, +}; + +export const createTeamEndpoint: ZodOpenApiOperationObject = { + operationId: "createTeam", + summary: "Create a team", + description: "Creates a team in the database.", + requestParams: { + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + }, + tags: ["Organizations API > Teams"], + requestBody: { + required: true, + description: "The team to create", + content: { + "application/json": { + schema: ZTeamInput, + }, + }, + }, + responses: { + "201": { + description: "Team created successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZTeam), + }, + }, + }, + }, +}; + +export const teamPaths: ZodOpenApiPathsObject = { + "/{organizationId}/teams": { + servers: organizationServer, + get: getTeamsEndpoint, + post: createTeamEndpoint, + }, + "/{organizationId}/teams/{id}": { + servers: organizationServer, + get: getTeamEndpoint, + put: updateTeamEndpoint, + delete: deleteTeamEndpoint, + }, +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts new file mode 100644 index 0000000000..db5812b3ef --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts @@ -0,0 +1,71 @@ +import "server-only"; +import { teamCache } from "@/lib/cache/team"; +import { getTeamsQuery } from "@/modules/api/v2/organizations/[organizationId]/teams/lib/utils"; +import { + TGetTeamsFilter, + TTeamInput, +} from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams"; +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; +import { Team } from "@prisma/client"; +import { prisma } from "@formbricks/database"; +import { organizationCache } from "@formbricks/lib/organization/cache"; +import { captureTelemetry } from "@formbricks/lib/telemetry"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const createTeam = async ( + teamInput: TTeamInput, + organizationId: string +): Promise> => { + captureTelemetry("team created"); + + const { name } = teamInput; + + try { + const team = await prisma.team.create({ + data: { + name, + organizationId, + }, + }); + + organizationCache.revalidate({ + id: organizationId, + }); + + teamCache.revalidate({ + organizationId: organizationId, + }); + + return ok(team); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "team", issue: error.message }] }); + } +}; + +export const getTeams = async ( + organizationId: string, + params: TGetTeamsFilter +): Promise, ApiErrorResponseV2>> => { + try { + const [teams, count] = await prisma.$transaction([ + prisma.team.findMany({ + ...getTeamsQuery(organizationId, params), + }), + prisma.team.count({ + where: getTeamsQuery(organizationId, params).where, + }), + ]); + + return ok({ + data: teams, + meta: { + total: count, + limit: params.limit, + offset: params.skip, + }, + }); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "teams", issue: error.message }] }); + } +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts new file mode 100644 index 0000000000..b7da581704 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/teams.test.ts @@ -0,0 +1,93 @@ +import { TGetTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { organizationCache } from "@formbricks/lib/organization/cache"; +import { createTeam, getTeams } from "../teams"; + +// Define a mock team object +const mockTeam = { + id: "team123", + organizationId: "org456", + name: "Test Team", +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +// Mock prisma methods +vi.mock("@formbricks/database", () => ({ + prisma: { + team: { + create: vi.fn(), + findMany: vi.fn(), + count: vi.fn(), + }, + $transaction: vi.fn(), + }, +})); + +// Mock organizationCache.revalidate +vi.spyOn(organizationCache, "revalidate").mockImplementation(() => {}); + +describe("Teams Lib", () => { + describe("createTeam", () => { + it("creates a team successfully and revalidates cache", async () => { + (prisma.team.create as any).mockResolvedValueOnce(mockTeam); + + const teamInput = { name: "Test Team" }; + const organizationId = "org456"; + const result = await createTeam(teamInput, organizationId); + expect(prisma.team.create).toHaveBeenCalledWith({ + data: { + name: "Test Team", + organizationId: organizationId, + }, + }); + expect(organizationCache.revalidate).toHaveBeenCalledWith({ id: organizationId }); + expect(result.ok).toBe(true); + if (result.ok) expect(result.data).toEqual(mockTeam); + }); + + it("returns internal error when prisma.team.create fails", async () => { + (prisma.team.create as any).mockRejectedValueOnce(new Error("Create error")); + const teamInput = { name: "Test Team" }; + const organizationId = "org456"; + const result = await createTeam(teamInput, organizationId); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toEqual("internal_server_error"); + } + }); + }); + + describe("getTeams", () => { + const filter = { limit: 10, skip: 0 }; + it("returns teams with meta on success", async () => { + const teamsArray = [mockTeam]; + // Simulate prisma transaction return [teams, count] + (prisma.$transaction as any).mockResolvedValueOnce([teamsArray, teamsArray.length]); + + const organizationId = "org456"; + const result = await getTeams(organizationId, filter as TGetTeamsFilter); + expect(prisma.$transaction).toHaveBeenCalled(); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data).toEqual({ + data: teamsArray, + meta: { total: teamsArray.length, limit: filter.limit, offset: filter.skip }, + }); + } + }); + + it("returns internal_server_error when prisma transaction fails", async () => { + (prisma.$transaction as any).mockRejectedValueOnce(new Error("Transaction error")); + const organizationId = "org456"; + const result = await getTeams(organizationId, filter as TGetTeamsFilter); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toEqual("internal_server_error"); + } + }); + }); +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts new file mode 100644 index 0000000000..4d77520d2d --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/tests/utils.test.ts @@ -0,0 +1,43 @@ +import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; +import { Prisma } from "@prisma/client"; +import { describe, expect, it, vi } from "vitest"; +import { getTeamsQuery } from "../utils"; + +// Mock the common utils functions +vi.mock("@/modules/api/v2/management/lib/utils", () => ({ + pickCommonFilter: vi.fn(), + buildCommonFilterQuery: vi.fn(), +})); + +describe("getTeamsQuery", () => { + const organizationId = "org123"; + + it("returns base query when no params provided", () => { + const result = getTeamsQuery(organizationId); + expect(result.where).toEqual({ organizationId }); + }); + + it("returns unchanged query if pickCommonFilter returns null/undefined", () => { + vi.mocked(pickCommonFilter).mockReturnValueOnce(null as any); + const params: any = { someParam: "test" }; + const result = getTeamsQuery(organizationId, params); + expect(pickCommonFilter).toHaveBeenCalledWith(params); + // Since pickCommonFilter returns undefined, query remains as base query. + expect(result.where).toEqual({ organizationId }); + }); + + it("calls buildCommonFilterQuery and returns updated query when base filter exists", () => { + const baseFilter = { key: "value" }; + vi.mocked(pickCommonFilter).mockReturnValueOnce(baseFilter as any); + // Simulate buildCommonFilterQuery to merge base query with baseFilter + const updatedQuery = { where: { organizationId, combined: true } } as Prisma.TeamFindManyArgs; + vi.mocked(buildCommonFilterQuery).mockReturnValueOnce(updatedQuery); + + const params: any = { someParam: "test" }; + const result = getTeamsQuery(organizationId, params); + + expect(pickCommonFilter).toHaveBeenCalledWith(params); + expect(buildCommonFilterQuery).toHaveBeenCalledWith({ where: { organizationId } }, baseFilter); + expect(result).toEqual(updatedQuery); + }); +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/utils.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/utils.ts new file mode 100644 index 0000000000..1e05db0401 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/utils.ts @@ -0,0 +1,21 @@ +import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; +import { TGetTeamsFilter } from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams"; +import { Prisma } from "@prisma/client"; + +export const getTeamsQuery = (organizationId: string, params?: TGetTeamsFilter) => { + let query: Prisma.TeamFindManyArgs = { + where: { + organizationId, + }, + }; + + if (!params) return query; + + const baseFilter = pickCommonFilter(params); + + if (baseFilter) { + query = buildCommonFilterQuery(query, baseFilter); + } + + return query; +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts new file mode 100644 index 0000000000..bf185277ac --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts @@ -0,0 +1,64 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; +import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; +import { hasOrganizationIdAndAccess } from "@/modules/api/v2/organizations/[organizationId]/lib/utils"; +import { createTeam, getTeams } from "@/modules/api/v2/organizations/[organizationId]/teams/lib/teams"; +import { + ZGetTeamsFilter, + ZTeamInput, +} from "@/modules/api/v2/organizations/[organizationId]/teams/types/teams"; +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { OrganizationAccessType } from "@formbricks/types/api-key"; + +export const GET = async (request: NextRequest, props: { params: Promise<{ organizationId: string }> }) => + authenticatedApiClient({ + request, + schemas: { + query: ZGetTeamsFilter.sourceType(), + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput: { query, params } }) => { + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Read)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const res = await getTeams(authentication.organizationId, query!); + + if (!res.ok) { + return handleApiError(request, res.error); + } + + return responses.successResponse(res.data); + }, + }); + +export const POST = async (request: Request, props: { params: Promise<{ organizationId: string }> }) => + authenticatedApiClient({ + request, + schemas: { + body: ZTeamInput, + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput: { body, params } }) => { + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const createTeamResult = await createTeam(body!, authentication.organizationId); + if (!createTeamResult.ok) { + return handleApiError(request, createTeamResult.error); + } + + return responses.successResponse({ data: createTeamResult.data, cors: true }); + }, + }); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/types/teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/types/teams.ts new file mode 100644 index 0000000000..60810b497d --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/types/teams.ts @@ -0,0 +1,23 @@ +import { ZGetFilter } from "@/modules/api/v2/types/api-filter"; +import { z } from "zod"; +import { ZTeam } from "@formbricks/database/zod/teams"; + +export const ZGetTeamsFilter = ZGetFilter.refine( + (data) => { + if (data.startDate && data.endDate && data.startDate > data.endDate) { + return false; + } + return true; + }, + { + message: "startDate must be before endDate", + } +); + +export type TGetTeamsFilter = z.infer; + +export const ZTeamInput = ZTeam.pick({ + name: true, +}); + +export type TTeamInput = z.infer; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/types/organizations.ts b/apps/web/modules/api/v2/organizations/[organizationId]/types/organizations.ts new file mode 100644 index 0000000000..60bc18ab45 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/types/organizations.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; +import { extendZodWithOpenApi } from "zod-openapi"; + +extendZodWithOpenApi(z); + +export const ZOrganizationIdSchema = z + .string() + .cuid2() + .openapi({ + ref: "organizationId", + description: "The ID of the organization", + param: { + name: "organizationId", + in: "path", + }, + }); diff --git a/apps/web/modules/api/v2/organizations/lib/openapi.ts b/apps/web/modules/api/v2/organizations/lib/openapi.ts new file mode 100644 index 0000000000..41354cf162 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/lib/openapi.ts @@ -0,0 +1,6 @@ +export const organizationServer = [ + { + url: "https://app.formbricks.com/api/v2/organizations", + description: "Formbricks Cloud", + }, +]; diff --git a/apps/web/modules/api/v2/management/roles/lib/openapi.ts b/apps/web/modules/api/v2/roles/lib/openapi.ts similarity index 72% rename from apps/web/modules/api/v2/management/roles/lib/openapi.ts rename to apps/web/modules/api/v2/roles/lib/openapi.ts index e7e937a924..8f45c1bc12 100644 --- a/apps/web/modules/api/v2/management/roles/lib/openapi.ts +++ b/apps/web/modules/api/v2/roles/lib/openapi.ts @@ -1,18 +1,19 @@ -import { z } from "zod"; +import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; +import { ZRoles } from "@formbricks/database/zod/roles"; export const getRolesEndpoint: ZodOpenApiOperationObject = { operationId: "getRoles", summary: "Get roles", description: "Gets roles from the database.", requestParams: {}, - tags: ["Management API > Roles"], + tags: ["Roles"], responses: { "200": { description: "Roles retrieved successfully.", content: { "application/json": { - schema: z.array(z.string()), + schema: makePartialSchema(ZRoles), }, }, }, diff --git a/apps/web/modules/api/v2/roles/lib/utils.ts b/apps/web/modules/api/v2/roles/lib/utils.ts new file mode 100644 index 0000000000..48eff88d75 --- /dev/null +++ b/apps/web/modules/api/v2/roles/lib/utils.ts @@ -0,0 +1,21 @@ +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { OrganizationRole } from "@prisma/client"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getRoles = (): Result<{ data: string[] }, ApiErrorResponseV2> => { + try { + const roles = Object.values(OrganizationRole); + + // Filter out the billing role if not in Formbricks Cloud + const filteredRoles = roles.filter((role) => !(role === "billing" && !IS_FORMBRICKS_CLOUD)); + return ok({ + data: filteredRoles, + }); + } catch { + return err({ + type: "internal_server_error", + details: [{ field: "roles", issue: "Failed to get roles" }], + }); + } +}; diff --git a/apps/web/modules/api/v2/management/roles/route.ts b/apps/web/modules/api/v2/roles/route.ts similarity index 67% rename from apps/web/modules/api/v2/management/roles/route.ts rename to apps/web/modules/api/v2/roles/route.ts index 829cbc2fe4..3989e1c37f 100644 --- a/apps/web/modules/api/v2/management/roles/route.ts +++ b/apps/web/modules/api/v2/roles/route.ts @@ -1,14 +1,14 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; -import { getRoles } from "@/modules/api/v2/management/roles/lib/roles"; +import { getRoles } from "@/modules/api/v2/roles/lib/utils"; import { NextRequest } from "next/server"; export const GET = async (request: NextRequest) => authenticatedApiClient({ request, handler: async () => { - const res = await getRoles(); + const res = getRoles(); if (res.ok) { return responses.successResponse(res.data); diff --git a/apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/route.ts b/apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/route.ts index 14286c0f2c..d030a433a6 100644 --- a/apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/route.ts +++ b/apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/route.ts @@ -1,6 +1,6 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; -import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; import { upsertBulkContacts } from "@/modules/ee/contacts/api/v2/management/contacts/bulk/lib/contact"; import { ZContactBulkUploadRequest } from "@/modules/ee/contacts/types/contact"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; diff --git a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx b/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx index 27f5e32bb9..406b57694d 100644 --- a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx +++ b/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx @@ -377,6 +377,7 @@ export const AddApiKeyModal = ({
setSelectedOrganizationAccessValue(key, "write", newVal) diff --git a/apps/web/modules/organization/settings/api-keys/lib/api-key.ts b/apps/web/modules/organization/settings/api-keys/lib/api-key.ts index 45ad8f1b77..0a2c8370bd 100644 --- a/apps/web/modules/organization/settings/api-keys/lib/api-key.ts +++ b/apps/web/modules/organization/settings/api-keys/lib/api-key.ts @@ -59,33 +59,50 @@ export const getApiKeyWithPermissions = reactCache(async (apiKey: string) => { const hashedKey = hashApiKey(apiKey); return cache( async () => { - // Look up the API key in the new structure - const apiKeyData = await prisma.apiKey.findUnique({ - where: { - hashedKey, - }, - include: { - apiKeyEnvironments: { - include: { - environment: true, + try { + // Look up the API key in the new structure + const apiKeyData = await prisma.apiKey.findUnique({ + where: { + hashedKey, + }, + include: { + apiKeyEnvironments: { + include: { + environment: { + include: { + project: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, }, }, - }, - }); + }); - if (!apiKeyData) return null; + if (!apiKeyData) return null; - // Update the last used timestamp - await prisma.apiKey.update({ - where: { - id: apiKeyData.id, - }, - data: { - lastUsedAt: new Date(), - }, - }); + // Update the last used timestamp + await prisma.apiKey.update({ + where: { + id: apiKeyData.id, + }, + data: { + lastUsedAt: new Date(), + }, + }); - return apiKeyData; + return apiKeyData; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } }, [`getApiKeyWithPermissions-${apiKey}`], { diff --git a/apps/web/playwright/api/constants.ts b/apps/web/playwright/api/constants.ts index caddcc1ed1..f46f162295 100644 --- a/apps/web/playwright/api/constants.ts +++ b/apps/web/playwright/api/constants.ts @@ -1,4 +1,9 @@ export const RESPONSES_API_URL = `/api/v2/management/responses`; export const SURVEYS_API_URL = `/api/v1/management/surveys`; export const WEBHOOKS_API_URL = `/api/v2/management/webhooks`; -export const ROLES_API_URL = `/api/v2/management/roles`; +export const ROLES_API_URL = `/api/v2/roles`; +export const ME_API_URL = `/api/v2/me`; + +export const TEAMS_API_URL = (organizationId: string) => `/api/v2/organizations/${organizationId}/teams`; +export const PROJECT_TEAMS_API_URL = (organizationId: string) => + `/api/v2/organizations/${organizationId}/project-teams`; diff --git a/apps/web/playwright/api/organization/project-team.spec.ts b/apps/web/playwright/api/organization/project-team.spec.ts new file mode 100644 index 0000000000..ebf5ce6043 --- /dev/null +++ b/apps/web/playwright/api/organization/project-team.spec.ts @@ -0,0 +1,120 @@ +import { ME_API_URL, PROJECT_TEAMS_API_URL, TEAMS_API_URL } from "@/playwright/api/constants"; +import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; +import { test } from "../../lib/fixtures"; +import { loginAndGetApiKey } from "../../lib/utils"; + +test.describe("API Tests for ProjectTeams", () => { + test("Create, Retrieve, Update, and Delete ProjectTeams via API", async ({ page, users, request }) => { + let apiKey; + try { + ({ apiKey } = await loginAndGetApiKey(page, users)); + } catch (error) { + logger.error(error, "Error logging in / retrieving API key"); + throw error; + } + + let organizationId, projectId, teamId: string; + + // Get organization ID using the me endpoint + await test.step("Get Organization ID", async () => { + const response = await request.get(ME_API_URL, { + headers: { + "x-api-key": apiKey, + }, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + + expect(responseBody.data).toBeTruthy(); + expect(responseBody.data.organizationId).toBeTruthy(); + + organizationId = responseBody.data.organizationId; + projectId = responseBody.data.environmentPermissions[0].projectId; + }); + + // Create a team to use for the project team + await test.step("Create Team via API", async () => { + const teamBody = { + organizationId: organizationId, + name: "New Team from API", + }; + + const response = await request.post(TEAMS_API_URL(organizationId), { + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + data: teamBody, + }); + + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.name).toEqual("New Team from API"); + teamId = responseBody.data.id; + }); + + await test.step("Create ProjectTeam via API", async () => { + const body = { + projectId: projectId, + teamId: teamId, + permission: "readWrite", + }; + const response = await request.post(PROJECT_TEAMS_API_URL(organizationId), { + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + data: body, + }); + + expect(response.ok()).toBe(true); + }); + + await test.step("Retrieve ProjectTeams via API", async () => { + const queryParams = { teamId: teamId, projectId: projectId }; + const response = await request.get(PROJECT_TEAMS_API_URL(organizationId), { + headers: { + "x-api-key": apiKey, + }, + params: queryParams, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(Array.isArray(responseBody.data)).toBe(true); + expect( + responseBody.data.find((pt: any) => pt.teamId === teamId && pt.projectId === projectId) + ).toBeTruthy(); + }); + + await test.step("Update ProjectTeam by ID via API", async () => { + const body = { + permission: "read", + }; + const queryParams = { teamId: teamId, projectId: projectId }; + const response = await request.put(`${PROJECT_TEAMS_API_URL(organizationId)}`, { + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + data: body, + params: queryParams, + }); + + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.permission).toBe("read"); + }); + + await test.step("Delete ProjectTeam via API", async () => { + const queryParams = { teamId: teamId, projectId: projectId }; + const response = await request.delete(`${PROJECT_TEAMS_API_URL(organizationId)}`, { + headers: { + "x-api-key": apiKey, + }, + params: queryParams, + }); + expect(response.ok()).toBe(true); + }); + }); +}); diff --git a/apps/web/playwright/api/organization/team.spec.ts b/apps/web/playwright/api/organization/team.spec.ts new file mode 100644 index 0000000000..4573f9dae6 --- /dev/null +++ b/apps/web/playwright/api/organization/team.spec.ts @@ -0,0 +1,108 @@ +import { ME_API_URL, TEAMS_API_URL } from "@/playwright/api/constants"; +import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; +import { test } from "../../lib/fixtures"; +import { loginAndGetApiKey } from "../../lib/utils"; + +test.describe("API Tests for Teams", () => { + test("Create, Retrieve, Update, and Delete Teams via API", async ({ page, users, request }) => { + let apiKey; + try { + ({ apiKey } = await loginAndGetApiKey(page, users)); + } catch (error) { + logger.error(error, "Error during login and getting API key"); + throw error; + } + + let organizationId, createdTeamId: string; + + // Get organization ID using the me endpoint + await test.step("Get Organization ID", async () => { + const response = await request.get(ME_API_URL, { + headers: { + "x-api-key": apiKey, + }, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + + expect(responseBody.data).toBeTruthy(); + expect(responseBody.data.organizationId).toBeTruthy(); + + organizationId = responseBody.data.organizationId; + }); + + await test.step("Create Team via API", async () => { + const teamBody = { + organizationId: organizationId, + name: "New Team from API", + }; + + const response = await request.post(TEAMS_API_URL(organizationId), { + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + data: teamBody, + }); + + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.name).toEqual("New Team from API"); + createdTeamId = responseBody.data.id; + }); + + await test.step("Retrieve Teams via API", async () => { + const queryParams = { limit: 10, skip: 0, sortBy: "createdAt", order: "asc" }; + + const response = await request.get(TEAMS_API_URL(organizationId), { + headers: { + "x-api-key": apiKey, + }, + params: queryParams, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(Array.isArray(responseBody.data)).toBe(true); + expect(responseBody.data.find((team: any) => team.id === createdTeamId)).toBeTruthy(); + }); + + await test.step("Update Team by ID via API", async () => { + const updatedTeamBody = { + name: "Updated Team from API", + }; + + const response = await request.put(`${TEAMS_API_URL(organizationId)}/${createdTeamId}`, { + headers: { + "x-api-key": apiKey, + "Content-Type": "application/json", + }, + data: updatedTeamBody, + }); + expect(response.ok()).toBe(true); + const responseJson = await response.json(); + expect(responseJson.data.name).toBe("Updated Team from API"); + }); + + await test.step("Get Team by ID from API", async () => { + const response = await request.get(`${TEAMS_API_URL(organizationId)}/${createdTeamId}`, { + headers: { + "x-api-key": apiKey, + }, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.id).toEqual(createdTeamId); + expect(responseBody.data.name).toEqual("Updated Team from API"); + }); + + await test.step("Delete Team via API", async () => { + const response = await request.delete(`${TEAMS_API_URL(organizationId)}/${createdTeamId}`, { + headers: { + "x-api-key": apiKey, + }, + }); + expect(response.ok()).toBe(true); + }); + }); +}); diff --git a/apps/web/playwright/api/management/role.spec.ts b/apps/web/playwright/api/role.spec.ts similarity index 89% rename from apps/web/playwright/api/management/role.spec.ts rename to apps/web/playwright/api/role.spec.ts index 8c02d0fa90..3e639172cf 100644 --- a/apps/web/playwright/api/management/role.spec.ts +++ b/apps/web/playwright/api/role.spec.ts @@ -1,8 +1,8 @@ import { ROLES_API_URL } from "@/playwright/api/constants"; import { expect } from "@playwright/test"; import { logger } from "@formbricks/logger"; -import { test } from "../../lib/fixtures"; -import { loginAndGetApiKey } from "../../lib/utils"; +import { test } from "../lib/fixtures"; +import { loginAndGetApiKey } from "../lib/utils"; test.describe("API Tests for Roles", () => { test("Retrieve Roles via API", async ({ page, users, request }) => { diff --git a/apps/web/playwright/lib/utils.ts b/apps/web/playwright/lib/utils.ts index 66b33c66a6..8420ec2aef 100644 --- a/apps/web/playwright/lib/utils.ts +++ b/apps/web/playwright/lib/utils.ts @@ -22,6 +22,8 @@ export async function loginAndGetApiKey(page: Page, users: UsersFixture) { await page.getByRole("menuitem", { name: "production" }).click(); await page.getByRole("button", { name: "read" }).click(); await page.getByRole("menuitem", { name: "manage" }).click(); + await page.getByTestId("organization-access-accessControl-read").click(); + await page.getByTestId("organization-access-accessControl-write").click(); await page.getByRole("button", { name: "Add API Key" }).click(); await page.locator(".copyApiKeyIcon").click(); diff --git a/docs/api-v2-reference/openapi.yml b/docs/api-v2-reference/openapi.yml index 62a01e4d24..e448d7f68a 100644 --- a/docs/api-v2-reference/openapi.yml +++ b/docs/api-v2-reference/openapi.yml @@ -7,6 +7,10 @@ servers: - url: https://app.formbricks.com/api/v2/management description: Formbricks Cloud tags: + - name: Roles + description: Operations for managing roles. + - name: Me + description: Operations for managing your API key. - name: Management API > Responses description: Operations for managing responses. - name: Management API > Contacts @@ -19,8 +23,10 @@ tags: description: Operations for managing surveys. - name: Management API > Webhooks description: Operations for managing webhooks. - - name: Management API > Roles - description: Operations for managing roles. + - name: Organizations API > Teams + description: Operations for managing teams. + - name: Organizations API > Project Teams + description: Operations for managing project teams. security: - apiKeyAuth: [] paths: @@ -523,6 +529,86 @@ paths: servers: - url: https://app.formbricks.com/api/v2 description: Formbricks API Server + /roles: + get: + operationId: getRoles + summary: Get roles + description: Gets roles from the database. + tags: + - Roles + responses: + "200": + description: Roles retrieved successfully. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: string + /me: + get: + operationId: me + summary: Me + description: Fetches the projects and organizations associated with the API key. + tags: + - Me + responses: + "200": + description: API key information retrieved successfully. + content: + application/json: + schema: + type: object + properties: + organizationId: + type: string + organizationAccess: + type: object + properties: + accessControl: + type: object + properties: + read: + type: boolean + write: + type: boolean + required: + - read + - write + additionalProperties: false + required: + - accessControl + environments: + type: array + items: + type: object + properties: + environmentId: + type: string + environmentType: + type: string + enum: + - production + - development + permission: + type: string + enum: + - read + - write + - manage + projectId: + type: string + projectName: + type: string + required: + - environmentId + - environmentType + - permission + - projectId + - projectName /responses: get: operationId: getResponses @@ -586,151 +672,149 @@ paths: content: application/json: schema: - type: array - items: - type: object - properties: - data: - type: array - items: - type: object - properties: - id: - type: string - description: The ID of the response - createdAt: - type: string - description: The date and time the response was created - example: 2021-01-01T00:00:00.000Z - updatedAt: - type: string - description: The date and time the response was last updated - example: 2021-01-01T00:00:00.000Z - finished: - type: boolean - description: Whether the response is finished - example: true - surveyId: - type: string - description: The ID of the survey - contactId: - type: - - string - - "null" - description: The ID of the contact - endingId: - type: - - string - - "null" - description: The ID of the ending - data: - type: object - additionalProperties: - anyOf: - - type: string - - type: number - - type: array - items: - type: string - - type: object - additionalProperties: - type: string - description: The data of the response - example: &a1 - question1: answer1 - question2: 2 - question3: - - answer3 - - answer4 - question4: - subquestion1: answer5 - variables: - type: object - additionalProperties: - anyOf: - - type: string - - type: number - description: The variables of the response - example: &a2 - variable1: answer1 - variable2: 2 - ttc: - type: object - additionalProperties: - type: number - description: The TTC of the response - example: &a3 - question1: 10 - question2: 20 - meta: - type: object - properties: - source: - type: string - description: The source of the response - example: https://example.com - url: - type: string - description: The URL of the response - example: https://example.com - userAgent: - type: object - properties: - browser: - type: string - os: - type: string - device: - type: string - country: - type: string - action: - type: string - description: The meta data of the response - example: &a4 - source: https://example.com - url: https://example.com - userAgent: - browser: Chrome - os: Windows - device: Desktop - country: US - action: click - contactAttributes: - type: - - object - - "null" - additionalProperties: - type: string - description: The attributes of the contact - example: &a5 - attribute1: value1 - attribute2: value2 - singleUseId: - type: - - string - - "null" - description: The single use ID of the response - language: - type: - - string - - "null" - description: The language of the response - example: en - displayId: - type: - - string - - "null" - description: The display ID of the response - meta: + type: object + properties: + data: + type: array + items: type: object properties: - total: - type: number - limit: - type: number - offset: - type: number + id: + type: string + description: The ID of the response + createdAt: + type: string + description: The date and time the response was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the response was last updated + example: 2021-01-01T00:00:00.000Z + finished: + type: boolean + description: Whether the response is finished + example: true + surveyId: + type: string + description: The ID of the survey + contactId: + type: + - string + - "null" + description: The ID of the contact + endingId: + type: + - string + - "null" + description: The ID of the ending + data: + type: object + additionalProperties: + anyOf: + - type: string + - type: number + - type: array + items: + type: string + - type: object + additionalProperties: + type: string + description: The data of the response + example: &a1 + question1: answer1 + question2: 2 + question3: + - answer3 + - answer4 + question4: + subquestion1: answer5 + variables: + type: object + additionalProperties: + anyOf: + - type: string + - type: number + description: The variables of the response + example: &a2 + variable1: answer1 + variable2: 2 + ttc: + type: object + additionalProperties: + type: number + description: The TTC of the response + example: &a3 + question1: 10 + question2: 20 + meta: + type: object + properties: + source: + type: string + description: The source of the response + example: https://example.com + url: + type: string + description: The URL of the response + example: https://example.com + userAgent: + type: object + properties: + browser: + type: string + os: + type: string + device: + type: string + country: + type: string + action: + type: string + description: The meta data of the response + example: &a4 + source: https://example.com + url: https://example.com + userAgent: + browser: Chrome + os: Windows + device: Desktop + country: US + action: click + contactAttributes: + type: + - object + - "null" + additionalProperties: + type: string + description: The attributes of the contact + example: &a5 + attribute1: value1 + attribute2: value2 + singleUseId: + type: + - string + - "null" + description: The single use ID of the response + language: + type: + - string + - "null" + description: The language of the response + example: en + displayId: + type: + - string + - "null" + description: The display ID of the response + meta: + type: object + properties: + total: + type: number + limit: + type: number + offset: + type: number post: operationId: createResponse summary: Create a response @@ -1418,6 +1502,106 @@ paths: - string - "null" description: The display ID of the response + /contacts/bulk: + put: + operationId: uploadBulkContacts + summary: Upload Bulk Contacts + description: Uploads contacts in bulk + tags: + - Management API > Contacts + requestBody: + required: true + description: The contacts to upload + content: + application/json: + schema: + type: object + properties: + environmentId: + type: string + contacts: + type: array + items: + type: object + properties: + attributes: + type: array + items: + type: object + properties: + attributeKey: + type: object + properties: + key: + type: string + name: + type: string + required: + - key + - name + value: + type: string + required: + - attributeKey + - value + required: + - attributes + maxItems: 1000 + required: + - environmentId + - contacts + responses: + "200": + description: Contacts uploaded successfully. + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + status: + type: string + message: + type: string + required: + - status + - message + required: + - data + "207": + description: Contacts uploaded partially successfully. + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + status: + type: string + message: + type: string + skippedContacts: + type: array + items: + type: object + properties: + index: + type: number + userId: + type: string + required: + - index + - userId + required: + - status + - message + - skippedContacts + required: + - data /contacts: get: operationId: getContacts @@ -2099,69 +2283,67 @@ paths: content: application/json: schema: - type: array - items: - type: object - properties: - data: - type: array - items: - type: object - properties: - id: - type: string - description: The ID of the webhook - name: - type: - - string - - "null" - description: The name of the webhook - createdAt: - type: string - description: The date and time the webhook was created - example: 2021-01-01T00:00:00.000Z - updatedAt: - type: string - description: The date and time the webhook was last updated - example: 2021-01-01T00:00:00.000Z - url: - type: string - format: uri - description: The URL of the webhook - source: - type: string - enum: &a8 - - user - - zapier - - make - - n8n - description: The source of the webhook - environmentId: - type: string - description: The ID of the environment - triggers: - type: array - items: - type: string - enum: &a9 - - responseFinished - - responseCreated - - responseUpdated - description: The triggers of the webhook - surveyIds: - type: array - items: - type: string - description: "The IDs of the surveys " - meta: + type: object + properties: + data: + type: array + items: type: object properties: - total: - type: number - limit: - type: number - offset: - type: number + id: + type: string + description: The ID of the webhook + name: + type: + - string + - "null" + description: The name of the webhook + createdAt: + type: string + description: The date and time the webhook was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the webhook was last updated + example: 2021-01-01T00:00:00.000Z + url: + type: string + format: uri + description: The URL of the webhook + source: + type: string + enum: &a8 + - user + - zapier + - make + - n8n + description: The source of the webhook + environmentId: + type: string + description: The ID of the environment + triggers: + type: array + items: + type: string + enum: &a9 + - responseFinished + - responseCreated + - responseUpdated + description: The triggers of the webhook + surveyIds: + type: array + items: + type: string + description: "The IDs of the surveys " + meta: + type: object + properties: + total: + type: number + limit: + type: number + offset: + type: number post: operationId: createWebhook summary: Create a webhook @@ -2256,7 +2438,7 @@ paths: items: type: string description: "The IDs of the surveys " - /webhooks/{webhookId}: + /webhooks/{id}: get: operationId: getWebhook summary: Get a webhook @@ -2476,22 +2658,593 @@ paths: items: type: string description: "The IDs of the surveys " - /roles: + /{organizationId}/teams: + servers: &a10 + - url: https://app.formbricks.com/api/v2/organizations + description: Formbricks Cloud get: - operationId: getRoles - summary: Get roles - description: Gets roles from the database. + operationId: getTeams + summary: Get teams + description: Gets teams from the database. tags: - - Management API > Roles + - Organizations API > Teams + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + - in: query + name: limit + schema: + type: number + minimum: 1 + maximum: 100 + default: 10 + - in: query + name: skip + schema: + type: number + minimum: 0 + default: 0 + - in: query + name: sortBy + schema: + type: string + enum: *a6 + default: createdAt + - in: query + name: order + schema: + type: string + enum: *a7 + default: desc + - in: query + name: startDate + schema: + type: string + required: true + - in: query + name: endDate + schema: + type: string + required: true responses: "200": - description: Roles retrieved successfully. + description: Teams retrieved successfully. content: application/json: schema: - type: array - items: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + description: The ID of the team + createdAt: + type: string + description: The date and time the team was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the team was last updated + example: 2021-01-01T00:00:00.000Z + name: + type: string + description: The name of the team + example: My team + organizationId: + type: string + description: The ID of the organization + meta: + type: object + properties: + total: + type: number + limit: + type: number + offset: + type: number + post: + operationId: createTeam + summary: Create a team + description: Creates a team in the database. + tags: + - Organizations API > Teams + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + requestBody: + required: true + description: The team to create + content: + application/json: + schema: + type: object + properties: + name: type: string + description: The name of the team + example: My team + required: + - name + responses: + "201": + description: Team created successfully. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: The ID of the team + createdAt: + type: string + description: The date and time the team was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the team was last updated + example: 2021-01-01T00:00:00.000Z + name: + type: string + description: The name of the team + example: My team + organizationId: + type: string + description: The ID of the organization + /{organizationId}/teams/{id}: + servers: *a10 + get: + operationId: getTeam + summary: Get a team + description: Gets a team from the database. + tags: + - Organizations API > Teams + parameters: + - in: path + name: id + description: The ID of the team + schema: + $ref: "#/components/schemas/teamId" + required: true + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + responses: + "200": + description: Team retrieved successfully. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: The ID of the team + createdAt: + type: string + description: The date and time the team was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the team was last updated + example: 2021-01-01T00:00:00.000Z + name: + type: string + description: The name of the team + example: My team + organizationId: + type: string + description: The ID of the organization + put: + operationId: updateTeam + summary: Update a team + description: Updates a team in the database. + tags: + - Organizations API > Teams + parameters: + - in: path + name: id + description: The ID of the team + schema: + $ref: "#/components/schemas/teamId" + required: true + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + requestBody: + required: true + description: The team to update + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: The name of the team + example: My team + required: + - name + responses: + "200": + description: Team updated successfully. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: The ID of the team + createdAt: + type: string + description: The date and time the team was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the team was last updated + example: 2021-01-01T00:00:00.000Z + name: + type: string + description: The name of the team + example: My team + organizationId: + type: string + description: The ID of the organization + delete: + operationId: deleteTeam + summary: Delete a team + description: Deletes a team from the database. + tags: + - Organizations API > Teams + parameters: + - in: path + name: id + description: The ID of the team + schema: + $ref: "#/components/schemas/teamId" + required: true + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + responses: + "200": + description: Team deleted successfully. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: The ID of the team + createdAt: + type: string + description: The date and time the team was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the team was last updated + example: 2021-01-01T00:00:00.000Z + name: + type: string + description: The name of the team + example: My team + organizationId: + type: string + description: The ID of the organization + /{organizationId}/project-teams: + servers: *a10 + get: + operationId: getProjectTeams + summary: Get project teams + description: Gets projectTeams from the database. + tags: + - Organizations API > Project Teams + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + - in: query + name: limit + schema: + type: number + minimum: 1 + maximum: 100 + default: 10 + - in: query + name: skip + schema: + type: number + minimum: 0 + default: 0 + - in: query + name: sortBy + schema: + type: string + enum: *a6 + default: createdAt + - in: query + name: order + schema: + type: string + enum: *a7 + default: desc + - in: query + name: startDate + schema: + type: string + required: true + - in: query + name: endDate + schema: + type: string + required: true + - in: query + name: teamId + schema: + type: string + required: true + - in: query + name: projectId + schema: + type: string + required: true + responses: + "200": + description: Project teams retrieved successfully. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + createdAt: + type: string + description: The date and time the project tem was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the project team was last updated + example: 2021-01-01T00:00:00.000Z + projectId: + type: string + description: The ID of the project + teamId: + type: string + description: The ID of the team + permission: + type: string + enum: + - read + - readWrite + - manage + description: Level of access granted to the project + meta: + type: object + properties: + total: + type: number + limit: + type: number + offset: + type: number + post: + operationId: createProjectTeam + summary: Create a projectTeam + description: Creates a project team in the database. + tags: + - Organizations API > Project Teams + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + requestBody: + required: true + description: The project team to create + content: + application/json: + schema: + type: object + properties: + teamId: + type: string + description: The ID of the team + projectId: + type: string + description: The ID of the project + permission: + type: string + enum: + - read + - readWrite + - manage + description: Level of access granted to the project + required: + - teamId + - projectId + - permission + responses: + "201": + description: Project team created successfully. + content: + application/json: + schema: + type: object + properties: + createdAt: + type: string + description: The date and time the project tem was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the project team was last updated + example: 2021-01-01T00:00:00.000Z + projectId: + type: string + description: The ID of the project + teamId: + type: string + description: The ID of the team + permission: + type: string + enum: + - read + - readWrite + - manage + description: Level of access granted to the project + put: + operationId: updateProjectTeam + summary: Update a project team + description: Updates a project team in the database. + tags: + - Organizations API > Project Teams + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + - in: query + name: teamId + schema: + type: string + required: true + - in: query + name: projectId + schema: + type: string + required: true + requestBody: + required: true + description: The project team to update + content: + application/json: + schema: + type: object + properties: + permission: + type: string + enum: + - read + - readWrite + - manage + description: Level of access granted to the project + required: + - permission + responses: + "200": + description: Project team updated successfully. + content: + application/json: + schema: + type: object + properties: + createdAt: + type: string + description: The date and time the project tem was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the project team was last updated + example: 2021-01-01T00:00:00.000Z + projectId: + type: string + description: The ID of the project + teamId: + type: string + description: The ID of the team + permission: + type: string + enum: + - read + - readWrite + - manage + description: Level of access granted to the project + delete: + operationId: deleteProjectTeam + summary: Delete a project team + description: Deletes a project team from the database. + tags: + - Organizations API > Project Teams + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + - in: query + name: teamId + schema: + type: string + required: true + - in: query + name: projectId + schema: + type: string + required: true + responses: + "200": + description: Project team deleted successfully. + content: + application/json: + schema: + type: object + properties: + createdAt: + type: string + description: The date and time the project tem was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the project team was last updated + example: 2021-01-01T00:00:00.000Z + projectId: + type: string + description: The ID of the project + teamId: + type: string + description: The ID of the team + permission: + type: string + enum: + - read + - readWrite + - manage + description: Level of access granted to the project components: securitySchemes: apiKeyAuth: @@ -2500,6 +3253,68 @@ components: name: x-api-key description: Use your Formbricks x-api-key to authenticate. schemas: + role: + type: object + properties: + data: + type: array + items: + type: string + required: + - data + me: + type: object + properties: + organizationId: + type: string + organizationAccess: + type: object + properties: + accessControl: + type: object + properties: + read: + type: boolean + write: + type: boolean + required: + - read + - write + additionalProperties: false + required: + - accessControl + environments: + type: array + items: + type: object + properties: + environmentId: + type: string + environmentType: + type: string + enum: + - production + - development + permission: + type: string + enum: + - read + - write + - manage + projectId: + type: string + projectName: + type: string + required: + - environmentId + - environmentType + - permission + - projectId + - projectName + required: + - organizationId + - organizationAccess + - environments response: type: object properties: @@ -2893,7 +3708,7 @@ components: required: - id - type - default: &a11 [] + default: &a12 [] description: The endings of the survey thankYouCard: type: @@ -2961,7 +3776,7 @@ components: description: Survey variables displayOption: type: string - enum: &a12 + enum: &a13 - displayOnce - displayMultiple - displaySome @@ -3040,12 +3855,13 @@ components: type: - string - "null" - enum: &a14 + enum: - bottomLeft - bottomRight - topLeft - topRight - center + - null clickOutsideClose: type: - boolean @@ -3195,13 +4011,13 @@ components: properties: linkSurveys: type: string - enum: &a10 + enum: &a11 - casual - straight - simple appSurveys: type: string - enum: *a10 + enum: *a11 required: - linkSurveys - appSurveys @@ -3218,11 +4034,12 @@ components: type: - string - "null" - enum: &a13 + enum: - animation - color - image - upload + - null brightness: type: - number @@ -3374,10 +4191,63 @@ components: - environmentId - triggers - surveyIds - role: - type: array - items: - type: string + team: + type: object + properties: + id: + type: string + description: The ID of the team + createdAt: + type: string + description: The date and time the team was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the team was last updated + example: 2021-01-01T00:00:00.000Z + name: + type: string + description: The name of the team + example: My team + organizationId: + type: string + description: The ID of the organization + required: + - id + - createdAt + - updatedAt + - name + - organizationId + projectTeam: + type: object + properties: + createdAt: + type: string + description: The date and time the project tem was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the project team was last updated + example: 2021-01-01T00:00:00.000Z + projectId: + type: string + description: The ID of the project + teamId: + type: string + description: The ID of the team + permission: + type: string + enum: + - read + - readWrite + - manage + description: Level of access granted to the project + required: + - createdAt + - updatedAt + - projectId + - teamId + - permission responseId: type: string description: The ID of the response @@ -3523,7 +4393,7 @@ components: required: - id - type - default: *a11 + default: *a12 description: The endings of the survey thankYouCard: type: @@ -3589,7 +4459,7 @@ components: description: Survey variables displayOption: type: string - enum: *a12 + enum: *a13 description: Display options for the survey recontactDays: type: @@ -3849,10 +4719,10 @@ components: properties: linkSurveys: type: string - enum: *a10 + enum: *a11 appSurveys: type: string - enum: *a10 + enum: *a11 required: - linkSurveys - appSurveys @@ -3869,7 +4739,12 @@ components: type: - string - "null" - enum: *a13 + enum: + - animation + - color + - image + - upload + - null brightness: type: - number @@ -3902,7 +4777,13 @@ components: type: - string - "null" - enum: *a14 + enum: + - bottomLeft + - bottomRight + - topLeft + - topRight + - center + - null clickOutsideClose: type: - boolean @@ -3936,3 +4817,9 @@ components: webhookId: type: string description: The ID of the webhook + organizationId: + type: string + description: The ID of the organization + teamId: + type: string + description: The ID of the team diff --git a/packages/database/zod/api-keys.ts b/packages/database/zod/api-keys.ts index db15dedde0..b15871f13e 100644 --- a/packages/database/zod/api-keys.ts +++ b/packages/database/zod/api-keys.ts @@ -1,4 +1,4 @@ -import { type ApiKey, type ApiKeyEnvironment, ApiKeyPermission } from "@prisma/client"; +import { type ApiKey, type ApiKeyEnvironment, ApiKeyPermission, EnvironmentType } from "@prisma/client"; import { z } from "zod"; import { ZOrganizationAccess } from "../../types/api-key"; @@ -10,6 +10,9 @@ export const ZApiKeyEnvironment = z.object({ updatedAt: z.date(), apiKeyId: z.string().cuid2(), environmentId: z.string().cuid2(), + projectId: z.string().cuid2(), + projectName: z.string(), + environmentType: z.nativeEnum(EnvironmentType), permission: ZApiKeyPermission, }) satisfies z.ZodType; @@ -37,3 +40,20 @@ export const ZApiKeyEnvironmentCreateInput = z.object({ environmentId: z.string().cuid2(), permission: ZApiKeyPermission, }); + +export const ZApiKeyData = ZApiKey.pick({ + organizationId: true, + organizationAccess: true, +}).merge( + z.object({ + environments: z.array( + ZApiKeyEnvironment.pick({ + environmentId: true, + environmentType: true, + permission: true, + projectId: true, + projectName: true, + }) + ), + }) +); diff --git a/packages/database/zod/project-teams.ts b/packages/database/zod/project-teams.ts new file mode 100644 index 0000000000..49647f8ec6 --- /dev/null +++ b/packages/database/zod/project-teams.ts @@ -0,0 +1,30 @@ +import { type ProjectTeam, ProjectTeamPermission } from "@prisma/client"; +import { z } from "zod"; +import { extendZodWithOpenApi } from "zod-openapi"; + +extendZodWithOpenApi(z); + +export const ZProjectTeam = z.object({ + createdAt: z.coerce.date().openapi({ + description: "The date and time the project tem was created", + example: "2021-01-01T00:00:00.000Z", + }), + updatedAt: z.coerce.date().openapi({ + description: "The date and time the project team was last updated", + example: "2021-01-01T00:00:00.000Z", + }), + projectId: z.string().cuid2().openapi({ + description: "The ID of the project", + }), + teamId: z.string().cuid2().openapi({ + description: "The ID of the team", + }), + permission: z.nativeEnum(ProjectTeamPermission).openapi({ + description: "Level of access granted to the project", + }), +}) satisfies z.ZodType; + +ZProjectTeam.openapi({ + ref: "projectTeam", + description: "A relationship between a project and a team with associated permissions", +}); diff --git a/packages/database/zod/roles.ts b/packages/database/zod/roles.ts new file mode 100644 index 0000000000..e085e4005f --- /dev/null +++ b/packages/database/zod/roles.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZRoles = z.object({ + data: z.array( + z.union([z.literal("owner"), z.literal("manager"), z.literal("member"), z.literal("billing")]) + ), +}); diff --git a/packages/database/zod/teams.ts b/packages/database/zod/teams.ts new file mode 100644 index 0000000000..d24e752cd7 --- /dev/null +++ b/packages/database/zod/teams.ts @@ -0,0 +1,31 @@ +import type { Team } from "@prisma/client"; +import { z } from "zod"; +import { extendZodWithOpenApi } from "zod-openapi"; + +extendZodWithOpenApi(z); + +export const ZTeam = z.object({ + id: z.string().cuid2().openapi({ + description: "The ID of the team", + }), + createdAt: z.coerce.date().openapi({ + description: "The date and time the team was created", + example: "2021-01-01T00:00:00.000Z", + }), + updatedAt: z.coerce.date().openapi({ + description: "The date and time the team was last updated", + example: "2021-01-01T00:00:00.000Z", + }), + name: z.string().openapi({ + description: "The name of the team", + example: "My team", + }), + organizationId: z.string().cuid2().openapi({ + description: "The ID of the organization", + }), +}) satisfies z.ZodType; + +ZTeam.openapi({ + ref: "team", + description: "A team", +}); diff --git a/packages/types/api-key.ts b/packages/types/api-key.ts index fbcb5a4df1..9e6731f0d5 100644 --- a/packages/types/api-key.ts +++ b/packages/types/api-key.ts @@ -1,11 +1,11 @@ import { z } from "zod"; -enum OrganizationAccessType { +export enum OrganizationAccessType { Read = "read", Write = "write", } -enum OrganizationAccess { +export enum OrganizationAccess { AccessControl = "accessControl", } diff --git a/packages/types/auth.ts b/packages/types/auth.ts index aef9184868..47b15a9ebd 100644 --- a/packages/types/auth.ts +++ b/packages/types/auth.ts @@ -1,5 +1,6 @@ -import { ApiKeyPermission } from "@prisma/client"; +import { ApiKeyPermission, EnvironmentType } from "@prisma/client"; import { z } from "zod"; +import { ZOrganizationAccess } from "./api-key"; import { ZUser } from "./user"; export const ZAuthSession = z.object({ @@ -8,6 +9,9 @@ export const ZAuthSession = z.object({ export const ZAPIKeyEnvironmentPermission = z.object({ environmentId: z.string(), + environmentType: z.nativeEnum(EnvironmentType), + projectId: z.string().cuid2(), + projectName: z.string(), permission: z.nativeEnum(ApiKeyPermission), }); @@ -17,8 +21,9 @@ export const ZAuthenticationApiKey = z.object({ type: z.literal("apiKey"), environmentPermissions: z.array(ZAPIKeyEnvironmentPermission), hashedApiKey: z.string(), - apiKeyId: z.string().optional(), - organizationId: z.string().optional(), + apiKeyId: z.string(), + organizationId: z.string(), + organizationAccess: ZOrganizationAccess, }); export type TAuthSession = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2768aebcb2..8ee8f0372b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -347,7 +347,7 @@ importers: version: 0.0.35(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@sentry/nextjs': specifier: 8.52.0 - version: 8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.25.2)) + version: 8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.25.2)) '@tailwindcss/forms': specifier: 0.5.9 version: 0.5.9(tailwindcss@3.4.16(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.8.2))) @@ -452,13 +452,13 @@ importers: version: 4.0.4 next: specifier: 15.2.4 - version: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-auth: specifier: 4.24.11 - version: 4.24.11(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 4.24.11(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-safe-action: specifier: 7.10.2 - version: 7.10.2(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1) + version: 7.10.2(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1) node-fetch: specifier: 3.3.2 version: 3.3.2 @@ -567,7 +567,7 @@ importers: version: link:../../packages/config-eslint '@neshca/cache-handler': specifier: 1.9.0 - version: 1.9.0(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0) + version: 1.9.0(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0) '@testing-library/react': specifier: 16.2.0 version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -16916,11 +16916,11 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@neshca/cache-handler@1.9.0(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0)': + '@neshca/cache-handler@1.9.0(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0)': dependencies: cluster-key-slot: 1.1.2 lru-cache: 10.4.3 - next: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) redis: 4.7.0 '@next/env@15.2.4': {} @@ -19136,7 +19136,7 @@ snapshots: '@sentry/core@8.52.0': {} - '@sentry/nextjs@8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.25.2))': + '@sentry/nextjs@8.52.0(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.97.1(esbuild@0.25.2))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.30.0 @@ -19149,7 +19149,7 @@ snapshots: '@sentry/vercel-edge': 8.52.0 '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.25.2)) chalk: 3.0.0 - next: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) resolve: 1.22.8 rollup: 3.29.5 stacktrace-parser: 0.1.11 @@ -25940,13 +25940,13 @@ snapshots: new-github-issue-url@0.2.1: {} - next-auth@4.24.11(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next-auth@4.24.11(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(nodemailer@6.9.16)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@babel/runtime': 7.27.0 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.25.2 @@ -25974,14 +25974,41 @@ snapshots: optionalDependencies: nodemailer: 6.10.0 - next-safe-action@7.10.2(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1): + next-safe-action@7.10.2(next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(zod@3.24.1): dependencies: - next: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: zod: 3.24.1 + next@15.2.4(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@next/env': 15.2.4 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001707 + postcss: 8.4.31 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.2.4 + '@next/swc-darwin-x64': 15.2.4 + '@next/swc-linux-arm64-gnu': 15.2.4 + '@next/swc-linux-arm64-musl': 15.2.4 + '@next/swc-linux-x64-gnu': 15.2.4 + '@next/swc-linux-x64-musl': 15.2.4 + '@next/swc-win32-arm64-msvc': 15.2.4 + '@next/swc-win32-x64-msvc': 15.2.4 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.51.1 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.2.4 @@ -28403,6 +28430,13 @@ snapshots: dependencies: inline-style-parser: 0.2.4 + styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0): + dependencies: + client-only: 0.0.1 + react: 19.0.0 + optionalDependencies: + '@babel/core': 7.26.0 + styled-jsx@5.1.6(react@19.0.0): dependencies: client-only: 0.0.1 diff --git a/sonar-project.properties b/sonar-project.properties index 3e6fde6dd1..bcce9834f1 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false sonar.sourceEncoding=UTF-8 # Coverage -sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/**, **/instrumentation.ts, scripts/merge-client-endpoints.ts -sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/openapi.ts,**/openapi-document.ts, **/instrumentation.ts, scripts/merge-client-endpoints.ts \ No newline at end of file +sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/**,**/instrumentation.ts,scripts/merge-client-endpoints.ts +sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts \ No newline at end of file From ec314c14eadf90e52acd93efababc98713b1be94 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Sat, 5 Apr 2025 21:20:22 +0900 Subject: [PATCH 147/411] fix: failing e2e test (#5234) --- apps/web/playwright/survey.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/playwright/survey.spec.ts b/apps/web/playwright/survey.spec.ts index f689a6eb27..6d277dde41 100644 --- a/apps/web/playwright/survey.spec.ts +++ b/apps/web/playwright/survey.spec.ts @@ -889,10 +889,10 @@ test.describe("Testing Survey with advanced logic", async () => { await page.goBack(); await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary(\?.*)?$/); - await page.waitForLoadState("networkidle"); + const currentUrl = page.url(); + const updatedUrl = currentUrl.replace("summary?share=true", "responses"); - await page.getByRole("button", { name: "Close" }).click(); - await page.getByRole("link").filter({ hasText: "Responses" }).click(); + await page.goto(updatedUrl); await page.waitForSelector("#response-table"); await expect(page.getByRole("cell", { name: "score" })).toBeVisible(); From c6538410371f8928a12cae7bd0bbf5a9f05c8a83 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Sun, 6 Apr 2025 09:52:53 +0530 Subject: [PATCH 148/411] chore: block signin with SSO when user is not found (#5233) Co-authored-by: pandeymangg --- .../modules/api/v2/management/lib/utils.ts | 6 +- apps/web/modules/auth/lib/authOptions.ts | 7 +- .../auth/login/components/login-form.tsx | 1 + .../auth/signup/components/signup-form.tsx | 1 + .../modules/auth/signup/lib/invite.test.ts | 246 +++++++++++++++++ apps/web/modules/auth/signup/lib/invite.ts | 51 +++- apps/web/modules/auth/signup/page.test.tsx | 154 +++++++++++ apps/web/modules/auth/signup/page.tsx | 17 +- .../ee/sso/components/azure-button.test.tsx | 84 ++++++ .../ee/sso/components/azure-button.tsx | 9 +- .../ee/sso/components/github-button.test.tsx | 76 ++++++ .../ee/sso/components/github-button.tsx | 8 +- .../ee/sso/components/google-button.test.tsx | 76 ++++++ .../ee/sso/components/google-button.tsx | 8 +- .../ee/sso/components/open-id-button.test.tsx | 91 +++++++ .../ee/sso/components/open-id-button.tsx | 16 +- .../ee/sso/components/saml-button.test.tsx | 130 +++++++++ .../modules/ee/sso/components/saml-button.tsx | 8 +- .../ee/sso/components/sso-options.test.tsx | 137 ++++++++++ .../modules/ee/sso/components/sso-options.tsx | 12 +- apps/web/modules/ee/sso/lib/sso-handlers.ts | 59 ++++- .../ee/sso/lib/tests/sso-handlers.test.ts | 247 ++++++++++++++++-- .../modules/ee/sso/lib/tests/utils.test.ts | 29 ++ apps/web/modules/ee/sso/lib/utils.ts | 5 + .../survey/hooks/useSingleUseId.test.tsx | 8 +- apps/web/vite.config.mts | 4 +- packages/lib/membership/service.ts | 13 + 27 files changed, 1450 insertions(+), 53 deletions(-) create mode 100644 apps/web/modules/auth/signup/lib/invite.test.ts create mode 100644 apps/web/modules/auth/signup/page.test.tsx create mode 100644 apps/web/modules/ee/sso/components/azure-button.test.tsx create mode 100644 apps/web/modules/ee/sso/components/github-button.test.tsx create mode 100644 apps/web/modules/ee/sso/components/google-button.test.tsx create mode 100644 apps/web/modules/ee/sso/components/open-id-button.test.tsx create mode 100644 apps/web/modules/ee/sso/components/saml-button.test.tsx create mode 100644 apps/web/modules/ee/sso/components/sso-options.test.tsx create mode 100644 apps/web/modules/ee/sso/lib/tests/utils.test.ts create mode 100644 apps/web/modules/ee/sso/lib/utils.ts diff --git a/apps/web/modules/api/v2/management/lib/utils.ts b/apps/web/modules/api/v2/management/lib/utils.ts index 33d5eb5fe8..bc10c929d7 100644 --- a/apps/web/modules/api/v2/management/lib/utils.ts +++ b/apps/web/modules/api/v2/management/lib/utils.ts @@ -9,7 +9,11 @@ export function pickCommonFilter(params: T) { return { limit, skip, sortBy, order, startDate, endDate }; } -type HasFindMany = Prisma.WebhookFindManyArgs | Prisma.ResponseFindManyArgs | Prisma.TeamFindManyArgs | Prisma.ProjectTeamFindManyArgs; +type HasFindMany = + | Prisma.WebhookFindManyArgs + | Prisma.ResponseFindManyArgs + | Prisma.TeamFindManyArgs + | Prisma.ProjectTeamFindManyArgs; export function buildCommonFilterQuery(query: T, params: TGetFilter): T { const { limit, skip, sortBy, order, startDate, endDate } = params || {}; diff --git a/apps/web/modules/auth/lib/authOptions.ts b/apps/web/modules/auth/lib/authOptions.ts index db9d31e98a..8711e00c8e 100644 --- a/apps/web/modules/auth/lib/authOptions.ts +++ b/apps/web/modules/auth/lib/authOptions.ts @@ -4,6 +4,7 @@ import { getSSOProviders } from "@/modules/ee/sso/lib/providers"; import { handleSsoCallback } from "@/modules/ee/sso/lib/sso-handlers"; import type { Account, NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; +import { cookies } from "next/headers"; import { prisma } from "@formbricks/database"; import { EMAIL_VERIFICATION_DISABLED, @@ -208,6 +209,10 @@ export const authOptions: NextAuthOptions = { return session; }, async signIn({ user, account }: { user: TUser; account: Account }) { + const cookieStore = await cookies(); + + const callbackUrl = cookieStore.get("next-auth.callback-url")?.value || ""; + if (account?.provider === "credentials" || account?.provider === "token") { // check if user's email is verified or not if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { @@ -217,7 +222,7 @@ export const authOptions: NextAuthOptions = { return true; } if (ENTERPRISE_LICENSE_KEY) { - const result = await handleSsoCallback({ user, account }); + const result = await handleSsoCallback({ user, account, callbackUrl }); if (result) { await updateUserLastLoginAt(user.email); } diff --git a/apps/web/modules/auth/login/components/login-form.tsx b/apps/web/modules/auth/login/components/login-form.tsx index d0eea0c62d..51d5db84ce 100644 --- a/apps/web/modules/auth/login/components/login-form.tsx +++ b/apps/web/modules/auth/login/components/login-form.tsx @@ -256,6 +256,7 @@ export const LoginForm = ({ samlTenant={samlTenant} samlProduct={samlProduct} callbackUrl={callbackUrl} + source="signin" /> )}
diff --git a/apps/web/modules/auth/signup/components/signup-form.tsx b/apps/web/modules/auth/signup/components/signup-form.tsx index 76fde04992..b2672dd5ea 100644 --- a/apps/web/modules/auth/signup/components/signup-form.tsx +++ b/apps/web/modules/auth/signup/components/signup-form.tsx @@ -280,6 +280,7 @@ export const SignupForm = ({ samlTenant={samlTenant} samlProduct={samlProduct} callbackUrl={callbackUrl} + source="signup" /> )} diff --git a/apps/web/modules/auth/signup/lib/invite.test.ts b/apps/web/modules/auth/signup/lib/invite.test.ts new file mode 100644 index 0000000000..e2628d8aed --- /dev/null +++ b/apps/web/modules/auth/signup/lib/invite.test.ts @@ -0,0 +1,246 @@ +import { inviteCache } from "@/lib/cache/invite"; +import { Prisma } from "@prisma/client"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { PrismaErrorType } from "@formbricks/database/types/error"; +import { logger } from "@formbricks/logger"; +import { DatabaseError } from "@formbricks/types/errors"; +import { deleteInvite, getInvite, getIsValidInviteToken } from "./invite"; + +// Mock data +const mockInviteId = "test-invite-id"; +const mockOrganizationId = "test-org-id"; +const mockCreatorId = "test-creator-id"; +const mockInvite = { + id: mockInviteId, + email: "test@test.com", + name: "Test Name", + organizationId: mockOrganizationId, + creatorId: mockCreatorId, + acceptorId: null, + createdAt: new Date(), + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours from now + deprecatedRole: null, + role: "member" as const, + teamIds: ["team-1"], + creator: { + name: "Test Creator", + email: "creator@test.com", + locale: "en", + }, +}; + +// Mock prisma methods +vi.mock("@formbricks/database", () => ({ + prisma: { + invite: { + delete: vi.fn(), + findUnique: vi.fn(), + }, + }, +})); + +// Mock cache +vi.mock("@/lib/cache/invite", () => ({ + inviteCache: { + revalidate: vi.fn(), + tag: { + byId: (id: string) => `invite-${id}`, + }, + }, +})); + +// Mock logger +vi.mock("@formbricks/logger", () => ({ + logger: { + error: vi.fn(), + }, +})); + +describe("Invite Management", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("deleteInvite", () => { + it("deletes an invite successfully and invalidates cache", async () => { + vi.mocked(prisma.invite.delete).mockResolvedValue(mockInvite); + + const result = await deleteInvite(mockInviteId); + + expect(result).toBe(true); + expect(prisma.invite.delete).toHaveBeenCalledWith({ + where: { id: mockInviteId }, + select: { id: true, organizationId: true }, + }); + expect(inviteCache.revalidate).toHaveBeenCalledWith({ + id: mockInviteId, + organizationId: mockOrganizationId, + }); + }); + + it("throws DatabaseError when invite doesn't exist", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Record not found", { + code: PrismaErrorType.RecordDoesNotExist, + clientVersion: "0.0.1", + }); + vi.mocked(prisma.invite.delete).mockRejectedValue(errToThrow); + + await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError); + }); + + it("throws DatabaseError for other Prisma errors", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Database error", { + code: "P2002", + clientVersion: "0.0.1", + }); + vi.mocked(prisma.invite.delete).mockRejectedValue(errToThrow); + + await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError); + }); + + it("throws DatabaseError for generic errors", async () => { + vi.mocked(prisma.invite.delete).mockRejectedValue(new Error("Generic error")); + + await expect(deleteInvite(mockInviteId)).rejects.toThrow(DatabaseError); + }); + }); + + describe("getInvite", () => { + it("retrieves an invite with creator details successfully", async () => { + vi.mocked(prisma.invite.findUnique).mockResolvedValue(mockInvite); + + const result = await getInvite(mockInviteId); + + expect(result).toEqual(mockInvite); + expect(prisma.invite.findUnique).toHaveBeenCalledWith({ + where: { id: mockInviteId }, + select: { + id: true, + organizationId: true, + role: true, + teamIds: true, + creator: { + select: { + name: true, + email: true, + locale: true, + }, + }, + }, + }); + }); + + it("returns null when invite doesn't exist", async () => { + vi.mocked(prisma.invite.findUnique).mockResolvedValue(null); + + const result = await getInvite(mockInviteId); + + expect(result).toBeNull(); + }); + + it("throws DatabaseError on prisma error", async () => { + const errToThrow = new Prisma.PrismaClientKnownRequestError("Database error", { + code: "P2002", + clientVersion: "0.0.1", + }); + vi.mocked(prisma.invite.findUnique).mockRejectedValue(errToThrow); + + await expect(getInvite(mockInviteId)).rejects.toThrow(DatabaseError); + }); + + it("throws DatabaseError for generic errors", async () => { + vi.mocked(prisma.invite.findUnique).mockRejectedValue(new Error("Generic error")); + + await expect(getInvite(mockInviteId)).rejects.toThrow(DatabaseError); + }); + }); + + describe("getIsValidInviteToken", () => { + it("returns true for valid invite", async () => { + vi.mocked(prisma.invite.findUnique).mockResolvedValue(mockInvite); + + const result = await getIsValidInviteToken(mockInviteId); + + expect(result).toBe(true); + expect(prisma.invite.findUnique).toHaveBeenCalledWith({ + where: { id: mockInviteId }, + }); + }); + + it("returns false when invite doesn't exist", async () => { + vi.mocked(prisma.invite.findUnique).mockResolvedValue(null); + + const result = await getIsValidInviteToken(mockInviteId); + + expect(result).toBe(false); + }); + + it("returns false for expired invite", async () => { + const expiredInvite = { + ...mockInvite, + expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago + }; + vi.mocked(prisma.invite.findUnique).mockResolvedValue(expiredInvite); + + const result = await getIsValidInviteToken(mockInviteId); + + expect(result).toBe(false); + expect(logger.error).toHaveBeenCalledWith( + { + inviteId: mockInviteId, + expiresAt: expiredInvite.expiresAt, + }, + "SSO: Invite token expired" + ); + }); + + it("returns false and logs error when database error occurs", async () => { + const error = new Error("Database error"); + vi.mocked(prisma.invite.findUnique).mockRejectedValue(error); + + const result = await getIsValidInviteToken(mockInviteId); + + expect(result).toBe(false); + expect(logger.error).toHaveBeenCalledWith(error, "Error getting invite"); + }); + + it("returns false for invite with null expiresAt", async () => { + const invalidInvite = { + ...mockInvite, + expiresAt: null, + }; + vi.mocked(prisma.invite.findUnique).mockResolvedValue(invalidInvite); + + const result = await getIsValidInviteToken(mockInviteId); + + expect(result).toBe(false); + expect(logger.error).toHaveBeenCalledWith( + { + inviteId: mockInviteId, + expiresAt: null, + }, + "SSO: Invite token expired" + ); + }); + + it("returns false for invite with invalid expiresAt", async () => { + const invalidInvite = { + ...mockInvite, + expiresAt: new Date("invalid-date"), + }; + vi.mocked(prisma.invite.findUnique).mockResolvedValue(invalidInvite); + + const result = await getIsValidInviteToken(mockInviteId); + + expect(result).toBe(false); + expect(logger.error).toHaveBeenCalledWith( + { + inviteId: mockInviteId, + expiresAt: invalidInvite.expiresAt, + }, + "SSO: Invite token expired" + ); + }); + }); +}); diff --git a/apps/web/modules/auth/signup/lib/invite.ts b/apps/web/modules/auth/signup/lib/invite.ts index 6a2cd7be03..fd879abbef 100644 --- a/apps/web/modules/auth/signup/lib/invite.ts +++ b/apps/web/modules/auth/signup/lib/invite.ts @@ -4,6 +4,7 @@ import { Prisma } from "@prisma/client"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { cache } from "@formbricks/lib/cache"; +import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; export const deleteInvite = async (inviteId: string): Promise => { @@ -32,8 +33,7 @@ export const deleteInvite = async (inviteId: string): Promise => { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); } - - throw error; + throw new DatabaseError(error instanceof Error ? error.message : "Unknown error occurred"); } }; @@ -66,8 +66,7 @@ export const getInvite = reactCache( if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); } - - throw error; + throw new DatabaseError(error instanceof Error ? error.message : "Unknown error occurred"); } }, [`signup-getInvite-${inviteId}`], @@ -76,3 +75,47 @@ export const getInvite = reactCache( } )() ); + +export const getIsValidInviteToken = reactCache( + async (inviteId: string): Promise => + cache( + async () => { + try { + const invite = await prisma.invite.findUnique({ + where: { id: inviteId }, + }); + if (!invite) { + return false; + } + if (!invite.expiresAt || isNaN(invite.expiresAt.getTime())) { + logger.error( + { + inviteId, + expiresAt: invite.expiresAt, + }, + "SSO: Invite token expired" + ); + return false; + } + if (invite.expiresAt < new Date()) { + logger.error( + { + inviteId, + expiresAt: invite.expiresAt, + }, + "SSO: Invite token expired" + ); + return false; + } + return true; + } catch (err) { + logger.error(err, "Error getting invite"); + return false; + } + }, + [`getIsValidInviteToken-${inviteId}`], + { + tags: [inviteCache.tag.byId(inviteId)], + } + )() +); diff --git a/apps/web/modules/auth/signup/page.test.tsx b/apps/web/modules/auth/signup/page.test.tsx new file mode 100644 index 0000000000..afe7955e43 --- /dev/null +++ b/apps/web/modules/auth/signup/page.test.tsx @@ -0,0 +1,154 @@ +import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite"; +import { + getIsMultiOrgEnabled, + getIsSamlSsoEnabled, + getisSsoEnabled, +} from "@/modules/ee/license-check/lib/utils"; +import { cleanup, render, screen } from "@testing-library/react"; +import { notFound } from "next/navigation"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { verifyInviteToken } from "@formbricks/lib/jwt"; +import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import { SignupPage } from "./page"; + +// Mock the necessary dependencies +vi.mock("@/modules/auth/components/testimonial", () => ({ + Testimonial: () =>
Testimonial
, +})); + +vi.mock("@/modules/auth/components/form-wrapper", () => ({ + FormWrapper: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +vi.mock("@/modules/auth/signup/components/signup-form", () => ({ + SignupForm: () =>
SignupForm
, +})); + +vi.mock("@/modules/ee/license-check/lib/utils", () => ({ + getIsMultiOrgEnabled: vi.fn(), + getIsSamlSsoEnabled: vi.fn(), + getisSsoEnabled: vi.fn(), +})); + +vi.mock("@/modules/auth/signup/lib/invite", () => ({ + getIsValidInviteToken: vi.fn(), +})); + +vi.mock("@formbricks/lib/jwt", () => ({ + verifyInviteToken: vi.fn(), +})); + +vi.mock("@formbricks/lib/utils/locale", () => ({ + findMatchingLocale: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + notFound: vi.fn(), +})); + +// Mock environment variables and constants +vi.mock("@formbricks/lib/constants", () => ({ + SIGNUP_ENABLED: true, + EMAIL_AUTH_ENABLED: true, + EMAIL_VERIFICATION_DISABLED: false, + GOOGLE_OAUTH_ENABLED: true, + GITHUB_OAUTH_ENABLED: true, + AZURE_OAUTH_ENABLED: true, + OIDC_OAUTH_ENABLED: true, + OIDC_DISPLAY_NAME: "OpenID", + SAML_OAUTH_ENABLED: true, + SAML_TENANT: "test-tenant", + SAML_PRODUCT: "test-product", + IS_TURNSTILE_CONFIGURED: true, + WEBAPP_URL: "http://localhost:3000", + TERMS_URL: "http://localhost:3000/terms", + PRIVACY_URL: "http://localhost:3000/privacy", + DEFAULT_ORGANIZATION_ID: "test-org-id", + DEFAULT_ORGANIZATION_ROLE: "admin", +})); + +describe("SignupPage", () => { + const mockSearchParams = { + inviteToken: "test-token", + email: "test@example.com", + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + cleanup(); + }); + + it("renders the signup page with all components when signup is enabled", async () => { + // Mock the license check functions to return true + vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); + vi.mocked(getisSsoEnabled).mockResolvedValue(true); + vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true); + vi.mocked(findMatchingLocale).mockResolvedValue("en"); + vi.mocked(verifyInviteToken).mockReturnValue({ inviteId: "test-invite-id" }); + vi.mocked(getIsValidInviteToken).mockResolvedValue(true); + + const result = await SignupPage({ searchParams: mockSearchParams }); + render(result); + + // Verify that all components are rendered + expect(screen.getByTestId("testimonial")).toBeInTheDocument(); + expect(screen.getByTestId("form-wrapper")).toBeInTheDocument(); + expect(screen.getByTestId("signup-form")).toBeInTheDocument(); + }); + + it("calls notFound when signup is disabled and no valid invite token is provided", async () => { + // Mock the license check functions to return false + vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); + vi.mocked(verifyInviteToken).mockImplementation(() => { + throw new Error("Invalid token"); + }); + + await SignupPage({ searchParams: {} }); + + expect(notFound).toHaveBeenCalled(); + }); + + it("calls notFound when invite token is invalid", async () => { + // Mock the license check functions to return false + vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); + vi.mocked(verifyInviteToken).mockImplementation(() => { + throw new Error("Invalid token"); + }); + + await SignupPage({ searchParams: { inviteToken: "invalid-token" } }); + + expect(notFound).toHaveBeenCalled(); + }); + + it("calls notFound when invite token is valid but invite is not found", async () => { + // Mock the license check functions to return false + vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(false); + vi.mocked(verifyInviteToken).mockReturnValue({ inviteId: "test-invite-id" }); + vi.mocked(getIsValidInviteToken).mockResolvedValue(false); + + await SignupPage({ searchParams: { inviteToken: "test-token" } }); + + expect(notFound).toHaveBeenCalled(); + }); + + it("renders the page with email from search params", async () => { + // Mock the license check functions to return true + vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true); + vi.mocked(getisSsoEnabled).mockResolvedValue(true); + vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true); + vi.mocked(findMatchingLocale).mockResolvedValue("en"); + vi.mocked(verifyInviteToken).mockReturnValue({ inviteId: "test-invite-id" }); + vi.mocked(getIsValidInviteToken).mockResolvedValue(true); + + const result = await SignupPage({ searchParams: { email: "test@example.com" } }); + render(result); + + // Verify that the form is rendered with the email from search params + expect(screen.getByTestId("signup-form")).toBeInTheDocument(); + }); +}); diff --git a/apps/web/modules/auth/signup/page.tsx b/apps/web/modules/auth/signup/page.tsx index d4f164fa5a..17e810ba2e 100644 --- a/apps/web/modules/auth/signup/page.tsx +++ b/apps/web/modules/auth/signup/page.tsx @@ -1,5 +1,6 @@ import { FormWrapper } from "@/modules/auth/components/form-wrapper"; import { Testimonial } from "@/modules/auth/components/testimonial"; +import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite"; import { getIsMultiOrgEnabled, getIsSamlSsoEnabled, @@ -25,6 +26,7 @@ import { TERMS_URL, WEBAPP_URL, } from "@formbricks/lib/constants"; +import { verifyInviteToken } from "@formbricks/lib/jwt"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; import { SignupForm } from "./components/signup-form"; @@ -38,11 +40,20 @@ export const SignupPage = async ({ searchParams: searchParamsProps }) => { ]); const samlSsoEnabled = isSamlSsoEnabled && SAML_OAUTH_ENABLED; - const locale = await findMatchingLocale(); - if (!inviteToken && (!SIGNUP_ENABLED || !isMultOrgEnabled)) { - notFound(); + if (!SIGNUP_ENABLED || !isMultOrgEnabled) { + if (!inviteToken) notFound(); + + try { + const { inviteId } = verifyInviteToken(inviteToken); + const isValidInviteToken = await getIsValidInviteToken(inviteId); + + if (!isValidInviteToken) notFound(); + } catch { + notFound(); + } } + const emailFromSearchParams = searchParams["email"]; return ( diff --git a/apps/web/modules/ee/sso/components/azure-button.test.tsx b/apps/web/modules/ee/sso/components/azure-button.test.tsx new file mode 100644 index 0000000000..bef70859f7 --- /dev/null +++ b/apps/web/modules/ee/sso/components/azure-button.test.tsx @@ -0,0 +1,84 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { signIn } from "next-auth/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; +import { AzureButton } from "./azure-button"; + +// Mock next-auth/react +vi.mock("next-auth/react", () => ({ + signIn: vi.fn(), +})); + +// Mock localStorage +const mockLocalStorage = { + setItem: vi.fn(), +}; +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, + writable: true, +}); + +describe("AzureButton", () => { + const defaultProps = { + source: "signin" as const, + }; + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it("renders correctly with default props", () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); + expect(button).toBeInTheDocument(); + }); + + it("renders with last used indicator when lastUsed is true", () => { + render(); + expect(screen.getByText("auth.last_used")).toBeInTheDocument(); + }); + + it("sets localStorage item and calls signIn on click", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); + fireEvent.click(button); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Azure"); + expect(signIn).toHaveBeenCalledWith("azure-ad", { + redirect: true, + callbackUrl: "/?source=signin", + }); + }); + + it("uses inviteUrl in callbackUrl when provided", async () => { + const inviteUrl = "https://example.com/invite"; + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("azure-ad", { + redirect: true, + callbackUrl: "https://example.com/invite?source=signin", + }); + }); + + it("handles signup source correctly", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_azure" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("azure-ad", { + redirect: true, + callbackUrl: "/?source=signup", + }); + }); + + it("triggers direct redirect when directRedirect is true", () => { + render(); + expect(signIn).toHaveBeenCalledWith("azure-ad", { + redirect: true, + callbackUrl: "/?source=signin", + }); + }); +}); diff --git a/apps/web/modules/ee/sso/components/azure-button.tsx b/apps/web/modules/ee/sso/components/azure-button.tsx index c6676213ae..84109ff94a 100644 --- a/apps/web/modules/ee/sso/components/azure-button.tsx +++ b/apps/web/modules/ee/sso/components/azure-button.tsx @@ -1,5 +1,6 @@ "use client"; +import { getCallbackUrl } from "@/modules/ee/sso/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { MicrosoftIcon } from "@/modules/ui/components/icons"; import { useTranslate } from "@tolgee/react"; @@ -11,20 +12,22 @@ interface AzureButtonProps { inviteUrl?: string; directRedirect?: boolean; lastUsed?: boolean; + source: "signin" | "signup"; } -export const AzureButton = ({ inviteUrl, directRedirect = false, lastUsed }: AzureButtonProps) => { +export const AzureButton = ({ inviteUrl, directRedirect = false, lastUsed, source }: AzureButtonProps) => { const { t } = useTranslate(); const handleLogin = useCallback(async () => { if (typeof window !== "undefined") { localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Azure"); } + const callbackUrlWithSource = getCallbackUrl(inviteUrl, source); await signIn("azure-ad", { redirect: true, - callbackUrl: inviteUrl ? inviteUrl : "/", + callbackUrl: callbackUrlWithSource, }); - }, [inviteUrl]); + }, [inviteUrl, source]); useEffect(() => { if (directRedirect) { diff --git a/apps/web/modules/ee/sso/components/github-button.test.tsx b/apps/web/modules/ee/sso/components/github-button.test.tsx new file mode 100644 index 0000000000..cde77b5ae6 --- /dev/null +++ b/apps/web/modules/ee/sso/components/github-button.test.tsx @@ -0,0 +1,76 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { signIn } from "next-auth/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; +import { GithubButton } from "./github-button"; + +// Mock next-auth/react +vi.mock("next-auth/react", () => ({ + signIn: vi.fn(), +})); + +// Mock localStorage +const mockLocalStorage = { + setItem: vi.fn(), +}; +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, + writable: true, +}); + +describe("GithubButton", () => { + const defaultProps = { + source: "signin" as const, + }; + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it("renders correctly with default props", () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_github" }); + expect(button).toBeInTheDocument(); + }); + + it("renders with last used indicator when lastUsed is true", () => { + render(); + expect(screen.getByText("auth.last_used")).toBeInTheDocument(); + }); + + it("sets localStorage item and calls signIn on click", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_github" }); + fireEvent.click(button); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Github"); + expect(signIn).toHaveBeenCalledWith("github", { + redirect: true, + callbackUrl: "/?source=signin", + }); + }); + + it("uses inviteUrl in callbackUrl when provided", async () => { + const inviteUrl = "https://example.com/invite"; + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_github" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("github", { + redirect: true, + callbackUrl: "https://example.com/invite?source=signin", + }); + }); + + it("handles signup source correctly", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_github" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("github", { + redirect: true, + callbackUrl: "/?source=signup", + }); + }); +}); diff --git a/apps/web/modules/ee/sso/components/github-button.tsx b/apps/web/modules/ee/sso/components/github-button.tsx index be497b5e31..e758a7f93a 100644 --- a/apps/web/modules/ee/sso/components/github-button.tsx +++ b/apps/web/modules/ee/sso/components/github-button.tsx @@ -1,5 +1,6 @@ "use client"; +import { getCallbackUrl } from "@/modules/ee/sso/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { GithubIcon } from "@/modules/ui/components/icons"; import { useTranslate } from "@tolgee/react"; @@ -9,17 +10,20 @@ import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; interface GithubButtonProps { inviteUrl?: string; lastUsed?: boolean; + source: "signin" | "signup"; } -export const GithubButton = ({ inviteUrl, lastUsed }: GithubButtonProps) => { +export const GithubButton = ({ inviteUrl, lastUsed, source }: GithubButtonProps) => { const { t } = useTranslate(); const handleLogin = async () => { if (typeof window !== "undefined") { localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Github"); } + const callbackUrlWithSource = getCallbackUrl(inviteUrl, source); + await signIn("github", { redirect: true, - callbackUrl: inviteUrl ? inviteUrl : "/", // redirect after login to / + callbackUrl: callbackUrlWithSource, }); }; diff --git a/apps/web/modules/ee/sso/components/google-button.test.tsx b/apps/web/modules/ee/sso/components/google-button.test.tsx new file mode 100644 index 0000000000..c6a8055d55 --- /dev/null +++ b/apps/web/modules/ee/sso/components/google-button.test.tsx @@ -0,0 +1,76 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { signIn } from "next-auth/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; +import { GoogleButton } from "./google-button"; + +// Mock next-auth/react +vi.mock("next-auth/react", () => ({ + signIn: vi.fn(), +})); + +// Mock localStorage +const mockLocalStorage = { + setItem: vi.fn(), +}; +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, + writable: true, +}); + +describe("GoogleButton", () => { + const defaultProps = { + source: "signin" as const, + }; + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it("renders correctly with default props", () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_google" }); + expect(button).toBeInTheDocument(); + }); + + it("renders with last used indicator when lastUsed is true", () => { + render(); + expect(screen.getByText("auth.last_used")).toBeInTheDocument(); + }); + + it("sets localStorage item and calls signIn on click", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_google" }); + fireEvent.click(button); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Google"); + expect(signIn).toHaveBeenCalledWith("google", { + redirect: true, + callbackUrl: "/?source=signin", + }); + }); + + it("uses inviteUrl in callbackUrl when provided", async () => { + const inviteUrl = "https://example.com/invite"; + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_google" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("google", { + redirect: true, + callbackUrl: "https://example.com/invite?source=signin", + }); + }); + + it("handles signup source correctly", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_google" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("google", { + redirect: true, + callbackUrl: "/?source=signup", + }); + }); +}); diff --git a/apps/web/modules/ee/sso/components/google-button.tsx b/apps/web/modules/ee/sso/components/google-button.tsx index 7b8e679e1f..5379311ec0 100644 --- a/apps/web/modules/ee/sso/components/google-button.tsx +++ b/apps/web/modules/ee/sso/components/google-button.tsx @@ -1,5 +1,6 @@ "use client"; +import { getCallbackUrl } from "@/modules/ee/sso/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { GoogleIcon } from "@/modules/ui/components/icons"; import { useTranslate } from "@tolgee/react"; @@ -9,17 +10,20 @@ import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; interface GoogleButtonProps { inviteUrl?: string; lastUsed?: boolean; + source: "signin" | "signup"; } -export const GoogleButton = ({ inviteUrl, lastUsed }: GoogleButtonProps) => { +export const GoogleButton = ({ inviteUrl, lastUsed, source }: GoogleButtonProps) => { const { t } = useTranslate(); const handleLogin = async () => { if (typeof window !== "undefined") { localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Google"); } + const callbackUrlWithSource = getCallbackUrl(inviteUrl, source); + await signIn("google", { redirect: true, - callbackUrl: inviteUrl ? inviteUrl : "/", // redirect after login to / + callbackUrl: callbackUrlWithSource, }); }; diff --git a/apps/web/modules/ee/sso/components/open-id-button.test.tsx b/apps/web/modules/ee/sso/components/open-id-button.test.tsx new file mode 100644 index 0000000000..4943794ec8 --- /dev/null +++ b/apps/web/modules/ee/sso/components/open-id-button.test.tsx @@ -0,0 +1,91 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { signIn } from "next-auth/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; +import { OpenIdButton } from "./open-id-button"; + +// Mock next-auth/react +vi.mock("next-auth/react", () => ({ + signIn: vi.fn(), +})); + +// Mock localStorage +const mockLocalStorage = { + setItem: vi.fn(), +}; +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, + writable: true, +}); + +describe("OpenIdButton", () => { + const defaultProps = { + source: "signin" as const, + }; + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it("renders correctly with default props", () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); + expect(button).toBeInTheDocument(); + }); + + it("renders with custom text when provided", () => { + const customText = "Custom OpenID Text"; + render(); + const button = screen.getByRole("button", { name: customText }); + expect(button).toBeInTheDocument(); + }); + + it("renders with last used indicator when lastUsed is true", () => { + render(); + expect(screen.getByText("auth.last_used")).toBeInTheDocument(); + }); + + it("sets localStorage item and calls signIn on click", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); + fireEvent.click(button); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "OpenID"); + expect(signIn).toHaveBeenCalledWith("openid", { + redirect: true, + callbackUrl: "/?source=signin", + }); + }); + + it("uses inviteUrl in callbackUrl when provided", async () => { + const inviteUrl = "https://example.com/invite"; + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("openid", { + redirect: true, + callbackUrl: "https://example.com/invite?source=signin", + }); + }); + + it("handles signup source correctly", async () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_openid" }); + fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith("openid", { + redirect: true, + callbackUrl: "/?source=signup", + }); + }); + + it("triggers direct redirect when directRedirect is true", () => { + render(); + expect(signIn).toHaveBeenCalledWith("openid", { + redirect: true, + callbackUrl: "/?source=signin", + }); + }); +}); diff --git a/apps/web/modules/ee/sso/components/open-id-button.tsx b/apps/web/modules/ee/sso/components/open-id-button.tsx index 588b452f36..b07258c5e2 100644 --- a/apps/web/modules/ee/sso/components/open-id-button.tsx +++ b/apps/web/modules/ee/sso/components/open-id-button.tsx @@ -1,5 +1,6 @@ "use client"; +import { getCallbackUrl } from "@/modules/ee/sso/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import { signIn } from "next-auth/react"; @@ -11,19 +12,28 @@ interface OpenIdButtonProps { lastUsed?: boolean; directRedirect?: boolean; text?: string; + source: "signin" | "signup"; } -export const OpenIdButton = ({ inviteUrl, lastUsed, directRedirect = false, text }: OpenIdButtonProps) => { +export const OpenIdButton = ({ + inviteUrl, + lastUsed, + directRedirect = false, + text, + source, +}: OpenIdButtonProps) => { const { t } = useTranslate(); const handleLogin = useCallback(async () => { if (typeof window !== "undefined") { localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "OpenID"); } + const callbackUrlWithSource = getCallbackUrl(inviteUrl, source); + await signIn("openid", { redirect: true, - callbackUrl: inviteUrl ? inviteUrl : "/", + callbackUrl: callbackUrlWithSource, }); - }, [inviteUrl]); + }, [inviteUrl, source]); useEffect(() => { if (directRedirect) { diff --git a/apps/web/modules/ee/sso/components/saml-button.test.tsx b/apps/web/modules/ee/sso/components/saml-button.test.tsx new file mode 100644 index 0000000000..5c7b707bc8 --- /dev/null +++ b/apps/web/modules/ee/sso/components/saml-button.test.tsx @@ -0,0 +1,130 @@ +import { doesSamlConnectionExistAction } from "@/modules/ee/sso/actions"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { signIn } from "next-auth/react"; +import toast from "react-hot-toast"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; +import { SamlButton } from "./saml-button"; + +// Mock next-auth/react +vi.mock("next-auth/react", () => ({ + signIn: vi.fn().mockResolvedValue(undefined), +})); + +// Mock localStorage +const mockLocalStorage = { + setItem: vi.fn(), +}; +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, + writable: true, +}); + +// Mock actions +vi.mock("@/modules/ee/sso/actions", () => ({ + doesSamlConnectionExistAction: vi.fn(), +})); + +// Mock toast +vi.mock("react-hot-toast", () => ({ + default: { + error: vi.fn(), + }, +})); + +describe("SamlButton", () => { + const defaultProps = { + source: "signin" as const, + samlTenant: "test-tenant", + samlProduct: "test-product", + }; + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it("renders correctly with default props", () => { + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); + expect(button).toBeInTheDocument(); + }); + + it("renders with last used indicator when lastUsed is true", () => { + render(); + expect(screen.getByText("auth.last_used")).toBeInTheDocument(); + }); + + it("sets localStorage item and calls signIn on click when SAML connection exists", async () => { + vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true }); + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); + + await fireEvent.click(button); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith(FORMBRICKS_LOGGED_IN_WITH_LS, "Saml"); + expect(signIn).toHaveBeenCalledWith( + "saml", + { + redirect: true, + callbackUrl: "/?source=signin", + }, + { + tenant: "test-tenant", + product: "test-product", + } + ); + }); + + it("shows error toast when SAML connection does not exist", async () => { + vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: false }); + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); + + await fireEvent.click(button); + + expect(toast.error).toHaveBeenCalledWith("auth.saml_connection_error"); + expect(signIn).not.toHaveBeenCalled(); + }); + + it("uses inviteUrl in callbackUrl when provided", async () => { + vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true }); + const inviteUrl = "https://example.com/invite"; + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); + + await fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith( + "saml", + { + redirect: true, + callbackUrl: "https://example.com/invite?source=signin", + }, + { + tenant: "test-tenant", + product: "test-product", + } + ); + }); + + it("handles signup source correctly", async () => { + vi.mocked(doesSamlConnectionExistAction).mockResolvedValue({ data: true }); + render(); + const button = screen.getByRole("button", { name: "auth.continue_with_saml" }); + + await fireEvent.click(button); + + expect(signIn).toHaveBeenCalledWith( + "saml", + { + redirect: true, + callbackUrl: "/?source=signup", + }, + { + tenant: "test-tenant", + product: "test-product", + } + ); + }); +}); diff --git a/apps/web/modules/ee/sso/components/saml-button.tsx b/apps/web/modules/ee/sso/components/saml-button.tsx index 6167e1cb20..258a6b80ed 100644 --- a/apps/web/modules/ee/sso/components/saml-button.tsx +++ b/apps/web/modules/ee/sso/components/saml-button.tsx @@ -1,6 +1,7 @@ "use client"; import { doesSamlConnectionExistAction } from "@/modules/ee/sso/actions"; +import { getCallbackUrl } from "@/modules/ee/sso/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { useTranslate } from "@tolgee/react"; import { LockIcon } from "lucide-react"; @@ -14,9 +15,10 @@ interface SamlButtonProps { lastUsed?: boolean; samlTenant: string; samlProduct: string; + source: "signin" | "signup"; } -export const SamlButton = ({ inviteUrl, lastUsed, samlTenant, samlProduct }: SamlButtonProps) => { +export const SamlButton = ({ inviteUrl, lastUsed, samlTenant, samlProduct, source }: SamlButtonProps) => { const { t } = useTranslate(); const [isLoading, setIsLoading] = useState(false); @@ -32,11 +34,13 @@ export const SamlButton = ({ inviteUrl, lastUsed, samlTenant, samlProduct }: Sam return; } + const callbackUrlWithSource = getCallbackUrl(inviteUrl, source); + signIn( "saml", { redirect: true, - callbackUrl: inviteUrl ? inviteUrl : "/", // redirect after login to / + callbackUrl: callbackUrlWithSource, }, { tenant: samlTenant, diff --git a/apps/web/modules/ee/sso/components/sso-options.test.tsx b/apps/web/modules/ee/sso/components/sso-options.test.tsx new file mode 100644 index 0000000000..458413d5a0 --- /dev/null +++ b/apps/web/modules/ee/sso/components/sso-options.test.tsx @@ -0,0 +1,137 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { SSOOptions } from "./sso-options"; + +// Mock environment variables +vi.mock("@formbricks/lib/env", () => ({ + env: { + IS_FORMBRICKS_CLOUD: "0", + }, +})); + +// Mock the translation hook +vi.mock("@tolgee/react", () => ({ + useTranslate: () => ({ + t: (key: string) => key, + }), +})); + +// Mock the individual SSO buttons +vi.mock("./google-button", () => ({ + GoogleButton: ({ lastUsed, source }: any) => ( +
+ Google Button +
+ ), +})); + +vi.mock("./github-button", () => ({ + GithubButton: ({ lastUsed, source }: any) => ( +
+ Github Button +
+ ), +})); + +vi.mock("./azure-button", () => ({ + AzureButton: ({ lastUsed, source }: any) => ( +
+ Azure Button +
+ ), +})); + +vi.mock("./open-id-button", () => ({ + OpenIdButton: ({ lastUsed, source, text }: any) => ( +
+ {text} +
+ ), +})); + +vi.mock("./saml-button", () => ({ + SamlButton: ({ lastUsed, source, samlTenant, samlProduct }: any) => ( +
+ Saml Button +
+ ), +})); + +describe("SSOOptions Component", () => { + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + const defaultProps = { + googleOAuthEnabled: true, + githubOAuthEnabled: true, + azureOAuthEnabled: true, + oidcOAuthEnabled: true, + oidcDisplayName: "OpenID", + callbackUrl: "http://localhost:3000", + samlSsoEnabled: true, + samlTenant: "test-tenant", + samlProduct: "test-product", + source: "signin" as const, + }; + + it("renders all SSO options when all are enabled", () => { + render(); + + expect(screen.getByTestId("google-button")).toBeInTheDocument(); + expect(screen.getByTestId("github-button")).toBeInTheDocument(); + expect(screen.getByTestId("azure-button")).toBeInTheDocument(); + expect(screen.getByTestId("openid-button")).toBeInTheDocument(); + expect(screen.getByTestId("saml-button")).toBeInTheDocument(); + }); + + it("only renders enabled SSO options", () => { + render( + + ); + + expect(screen.queryByTestId("google-button")).not.toBeInTheDocument(); + expect(screen.queryByTestId("github-button")).not.toBeInTheDocument(); + expect(screen.queryByTestId("azure-button")).not.toBeInTheDocument(); + expect(screen.getByTestId("openid-button")).toBeInTheDocument(); + expect(screen.getByTestId("saml-button")).toBeInTheDocument(); + }); + + it("passes correct props to OpenID button", () => { + render(); + const openIdButton = screen.getByTestId("openid-button"); + + expect(openIdButton).toHaveAttribute("data-source", "signin"); + expect(openIdButton).toHaveTextContent("auth.continue_with_oidc"); + }); + + it("passes correct props to SAML button", () => { + render(); + const samlButton = screen.getByTestId("saml-button"); + + expect(samlButton).toHaveAttribute("data-source", "signin"); + expect(samlButton).toHaveAttribute("data-tenant", "test-tenant"); + expect(samlButton).toHaveAttribute("data-product", "test-product"); + }); + + it("passes correct source prop to all buttons", () => { + render(); + + expect(screen.getByTestId("google-button")).toHaveAttribute("data-source", "signup"); + expect(screen.getByTestId("github-button")).toHaveAttribute("data-source", "signup"); + expect(screen.getByTestId("azure-button")).toHaveAttribute("data-source", "signup"); + expect(screen.getByTestId("openid-button")).toHaveAttribute("data-source", "signup"); + expect(screen.getByTestId("saml-button")).toHaveAttribute("data-source", "signup"); + }); +}); diff --git a/apps/web/modules/ee/sso/components/sso-options.tsx b/apps/web/modules/ee/sso/components/sso-options.tsx index 8f3c490520..cf0cb40579 100644 --- a/apps/web/modules/ee/sso/components/sso-options.tsx +++ b/apps/web/modules/ee/sso/components/sso-options.tsx @@ -19,6 +19,7 @@ interface SSOOptionsProps { samlSsoEnabled: boolean; samlTenant: string; samlProduct: string; + source: "signin" | "signup"; } export const SSOOptions = ({ @@ -31,6 +32,7 @@ export const SSOOptions = ({ samlSsoEnabled, samlTenant, samlProduct, + source, }: SSOOptionsProps) => { const { t } = useTranslate(); const [lastLoggedInWith, setLastLoggedInWith] = useState(""); @@ -44,17 +46,20 @@ export const SSOOptions = ({ return (
{googleOAuthEnabled && ( - + )} {githubOAuthEnabled && ( - + + )} + {azureOAuthEnabled && ( + )} - {azureOAuthEnabled && } {oidcOAuthEnabled && ( )} {samlSsoEnabled && ( @@ -63,6 +68,7 @@ export const SSOOptions = ({ lastUsed={lastLoggedInWith === "Saml"} samlTenant={samlTenant} samlProduct={samlProduct} + source={source} /> )}
diff --git a/apps/web/modules/ee/sso/lib/sso-handlers.ts b/apps/web/modules/ee/sso/lib/sso-handlers.ts index 35bb9920a0..b5500f34cb 100644 --- a/apps/web/modules/ee/sso/lib/sso-handlers.ts +++ b/apps/web/modules/ee/sso/lib/sso-handlers.ts @@ -1,19 +1,34 @@ import { createBrevoCustomer } from "@/modules/auth/lib/brevo"; import { getUserByEmail, updateUser } from "@/modules/auth/lib/user"; import { createUser } from "@/modules/auth/lib/user"; +import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite"; import { TOidcNameFields, TSamlNameFields } from "@/modules/auth/types/auth"; -import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils"; +import { + getIsMultiOrgEnabled, + getIsSamlSsoEnabled, + getisSsoEnabled, +} from "@/modules/ee/license-check/lib/utils"; import type { IdentityProvider } from "@prisma/client"; import type { Account } from "next-auth"; import { prisma } from "@formbricks/database"; import { createAccount } from "@formbricks/lib/account/service"; import { DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_ROLE } from "@formbricks/lib/constants"; +import { verifyInviteToken } from "@formbricks/lib/jwt"; import { createMembership } from "@formbricks/lib/membership/service"; import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import { logger } from "@formbricks/logger"; import type { TUser, TUserNotificationSettings } from "@formbricks/types/user"; -export const handleSsoCallback = async ({ user, account }: { user: TUser; account: Account }) => { +export const handleSsoCallback = async ({ + user, + account, + callbackUrl, +}: { + user: TUser; + account: Account; + callbackUrl: string; +}) => { const isSsoEnabled = await getisSsoEnabled(); if (!isSsoEnabled) { return false; @@ -102,6 +117,46 @@ export const handleSsoCallback = async ({ user, account }: { user: TUser; accoun } } + // Get multi-org license status + const isMultiOrgEnabled = await getIsMultiOrgEnabled(); + + // Reject if no callback URL and no default org in self-hosted environment + if (!callbackUrl && !DEFAULT_ORGANIZATION_ID && !isMultiOrgEnabled) { + return false; + } + + // Additional security checks for self-hosted instances without default org + if (!DEFAULT_ORGANIZATION_ID && !isMultiOrgEnabled) { + try { + // Parse and validate the callback URL + const isValidCallbackUrl = new URL(callbackUrl); + // Extract invite token and source from URL parameters + const inviteToken = isValidCallbackUrl.searchParams.get("token") || ""; + const source = isValidCallbackUrl.searchParams.get("source") || ""; + + // Allow sign-in if multi-org is enabled, otherwise check for invite token + if (source === "signin" && !inviteToken) { + return false; + } + + // If multi-org is enabled, skip invite token validation + // Verify invite token and check email match + const { email, inviteId } = verifyInviteToken(inviteToken); + if (email !== user.email) { + return false; + } + // Check if invite token is still valid + const isValidInviteToken = await getIsValidInviteToken(inviteId); + if (!isValidInviteToken) { + return false; + } + } catch (err) { + // Log and reject on any validation errors + logger.error(err, "Invalid callbackUrl"); + return false; + } + } + const userProfile = await createUser({ name: userName || diff --git a/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts b/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts index 318a4a9143..3bb8b5ef53 100644 --- a/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts +++ b/apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts @@ -1,5 +1,6 @@ import { createBrevoCustomer } from "@/modules/auth/lib/brevo"; import { createUser, getUserByEmail, updateUser } from "@/modules/auth/lib/user"; +import type { TSamlNameFields } from "@/modules/auth/types/auth"; import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { prisma } from "@formbricks/database"; @@ -7,6 +8,7 @@ import { createAccount } from "@formbricks/lib/account/service"; import { createMembership } from "@formbricks/lib/membership/service"; import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import type { TUser } from "@formbricks/types/user"; import { handleSsoCallback } from "../sso-handlers"; import { mockAccount, @@ -32,6 +34,7 @@ vi.mock("@/modules/auth/lib/user", () => ({ vi.mock("@/modules/ee/license-check/lib/utils", () => ({ getIsSamlSsoEnabled: vi.fn(), getisSsoEnabled: vi.fn(), + getIsMultiOrgEnabled: vi.fn().mockResolvedValue(true), })); vi.mock("@formbricks/database", () => ({ @@ -63,6 +66,7 @@ vi.mock("@formbricks/lib/utils/locale", () => ({ vi.mock("@formbricks/lib/constants", () => ({ DEFAULT_ORGANIZATION_ID: "org-123", DEFAULT_ORGANIZATION_ROLE: "member", + ENCRYPTION_KEY: "test-encryption-key-32-chars-long", })); describe("handleSsoCallback", () => { @@ -90,24 +94,31 @@ describe("handleSsoCallback", () => { it("should return false if SSO is not enabled", async () => { vi.mocked(getisSsoEnabled).mockResolvedValue(false); - const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(false); - expect(getisSsoEnabled).toHaveBeenCalled(); }); it("should return false if user email is missing", async () => { - const userWithoutEmail = { ...mockUser, email: "" }; - - const result = await handleSsoCallback({ user: userWithoutEmail, account: mockAccount }); + const result = await handleSsoCallback({ + user: { ...mockUser, email: "" }, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(false); }); it("should return false if account type is not oauth", async () => { - const nonOauthAccount = { ...mockAccount, type: "credentials" as const }; - - const result = await handleSsoCallback({ user: mockUser, account: nonOauthAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: { ...mockAccount, type: "credentials" }, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(false); }); @@ -115,10 +126,13 @@ describe("handleSsoCallback", () => { it("should return false if provider is SAML and SAML SSO is not enabled", async () => { vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(false); - const result = await handleSsoCallback({ user: mockUser, account: mockSamlAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockSamlAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(false); - expect(getIsSamlSsoEnabled).toHaveBeenCalled(); }); }); @@ -130,7 +144,11 @@ describe("handleSsoCallback", () => { accounts: [{ provider: mockAccount.provider }], }); - const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(prisma.user.findFirst).toHaveBeenCalledWith({ @@ -160,7 +178,11 @@ describe("handleSsoCallback", () => { vi.mocked(getUserByEmail).mockResolvedValue(null); vi.mocked(updateUser).mockResolvedValue({ ...existingUser, email: mockUser.email }); - const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(updateUser).toHaveBeenCalledWith(existingUser.id, { email: mockUser.email }); @@ -180,9 +202,16 @@ describe("handleSsoCallback", () => { email: mockUser.email, emailVerified: mockUser.emailVerified, locale: mockUser.locale, + isActive: true, }); - await expect(handleSsoCallback({ user: mockUser, account: mockAccount })).rejects.toThrow( + await expect( + handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }) + ).rejects.toThrow( "Looks like you updated your email somewhere else. A user with this new email exists already." ); }); @@ -194,9 +223,14 @@ describe("handleSsoCallback", () => { email: mockUser.email, emailVerified: mockUser.emailVerified, locale: mockUser.locale, + isActive: true, }); - const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); }); @@ -208,7 +242,11 @@ describe("handleSsoCallback", () => { vi.mocked(getUserByEmail).mockResolvedValue(null); vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); - const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith({ @@ -228,7 +266,11 @@ describe("handleSsoCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); vi.mocked(getOrganization).mockResolvedValue(null); - const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(createOrganization).toHaveBeenCalledWith({ @@ -255,7 +297,11 @@ describe("handleSsoCallback", () => { vi.mocked(getUserByEmail).mockResolvedValue(null); vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); - const result = await handleSsoCallback({ user: mockUser, account: mockAccount }); + const result = await handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(createOrganization).not.toHaveBeenCalled(); @@ -276,14 +322,21 @@ describe("handleSsoCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser("Direct Name")); - const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ + user: openIdUser, + account: mockOpenIdAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ name: "Direct Name", email: openIdUser.email, + emailVerified: expect.any(Date), identityProvider: "openid", + identityProviderAccountId: mockOpenIdAccount.providerAccountId, + locale: "en-US", }) ); }); @@ -297,14 +350,21 @@ describe("handleSsoCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser("John Doe")); - const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ + user: openIdUser, + account: mockOpenIdAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ name: "John Doe", email: openIdUser.email, + emailVerified: expect.any(Date), identityProvider: "openid", + identityProviderAccountId: mockOpenIdAccount.providerAccountId, + locale: "en-US", }) ); }); @@ -319,14 +379,21 @@ describe("handleSsoCallback", () => { vi.mocked(createUser).mockResolvedValue(mockCreatedUser("preferred.user")); - const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ + user: openIdUser, + account: mockOpenIdAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ name: "preferred.user", email: openIdUser.email, + emailVerified: expect.any(Date), identityProvider: "openid", + identityProviderAccountId: mockOpenIdAccount.providerAccountId, + locale: "en-US", }) ); }); @@ -340,18 +407,152 @@ describe("handleSsoCallback", () => { email: "test.user@example.com", }); - vi.mocked(createUser).mockResolvedValue(mockCreatedUser("test.user")); + vi.mocked(createUser).mockResolvedValue(mockCreatedUser("test user")); - const result = await handleSsoCallback({ user: openIdUser, account: mockOpenIdAccount }); + const result = await handleSsoCallback({ + user: openIdUser, + account: mockOpenIdAccount, + callbackUrl: "http://localhost:3000", + }); expect(result).toBe(true); - expect(createUser).toHaveBeenCalledWith( expect.objectContaining({ + name: "test user", email: openIdUser.email, + emailVerified: expect.any(Date), identityProvider: "openid", + identityProviderAccountId: mockOpenIdAccount.providerAccountId, + locale: "en-US", }) ); }); }); + + describe("SAML name handling", () => { + it("should use samlUser.name when available", async () => { + const samlUser = { + ...mockUser, + name: "Direct Name", + firstName: "John", + lastName: "Doe", + } as TUser & TSamlNameFields; + + vi.mocked(createUser).mockResolvedValue(mockCreatedUser("Direct Name")); + + const result = await handleSsoCallback({ + user: samlUser, + account: mockSamlAccount, + callbackUrl: "http://localhost:3000", + }); + + expect(result).toBe(true); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ + name: "Direct Name", + email: samlUser.email, + emailVerified: expect.any(Date), + identityProvider: "saml", + identityProviderAccountId: mockSamlAccount.providerAccountId, + locale: "en-US", + }) + ); + }); + + it("should use firstName + lastName when name is not available", async () => { + const samlUser = { + ...mockUser, + name: "", + firstName: "John", + lastName: "Doe", + } as TUser & TSamlNameFields; + + vi.mocked(createUser).mockResolvedValue(mockCreatedUser("John Doe")); + + const result = await handleSsoCallback({ + user: samlUser, + account: mockSamlAccount, + callbackUrl: "http://localhost:3000", + }); + + expect(result).toBe(true); + expect(createUser).toHaveBeenCalledWith( + expect.objectContaining({ + name: "John Doe", + email: samlUser.email, + emailVerified: expect.any(Date), + identityProvider: "saml", + identityProviderAccountId: mockSamlAccount.providerAccountId, + locale: "en-US", + }) + ); + }); + }); + + describe("Organization handling", () => { + it("should handle invalid DEFAULT_ORGANIZATION_ID gracefully", async () => { + vi.mocked(prisma.user.findFirst).mockResolvedValue(null); + vi.mocked(getUserByEmail).mockResolvedValue(null); + vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); + vi.mocked(getOrganization).mockResolvedValue(null); + vi.mocked(createOrganization).mockRejectedValue(new Error("Invalid organization ID")); + + await expect( + handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }) + ).rejects.toThrow("Invalid organization ID"); + + expect(createOrganization).toHaveBeenCalled(); + expect(createMembership).not.toHaveBeenCalled(); + }); + + it("should handle membership creation failure gracefully", async () => { + vi.mocked(prisma.user.findFirst).mockResolvedValue(null); + vi.mocked(getUserByEmail).mockResolvedValue(null); + vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); + vi.mocked(createMembership).mockRejectedValue(new Error("Failed to create membership")); + + await expect( + handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }) + ).rejects.toThrow("Failed to create membership"); + + expect(createMembership).toHaveBeenCalled(); + }); + }); + + describe("Error handling", () => { + it("should handle prisma errors gracefully", async () => { + vi.mocked(prisma.user.findFirst).mockRejectedValue(new Error("Database error")); + + await expect( + handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }) + ).rejects.toThrow("Database error"); + }); + + it("should handle locale finding errors gracefully", async () => { + vi.mocked(findMatchingLocale).mockRejectedValue(new Error("Locale error")); + vi.mocked(prisma.user.findFirst).mockResolvedValue(null); + vi.mocked(getUserByEmail).mockResolvedValue(null); + vi.mocked(createUser).mockResolvedValue(mockCreatedUser()); + + await expect( + handleSsoCallback({ + user: mockUser, + account: mockAccount, + callbackUrl: "http://localhost:3000", + }) + ).rejects.toThrow("Locale error"); + }); + }); }); diff --git a/apps/web/modules/ee/sso/lib/tests/utils.test.ts b/apps/web/modules/ee/sso/lib/tests/utils.test.ts new file mode 100644 index 0000000000..6d263ef4e0 --- /dev/null +++ b/apps/web/modules/ee/sso/lib/tests/utils.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { getCallbackUrl } from "../utils"; + +describe("getCallbackUrl", () => { + it("should return base URL with source when no inviteUrl is provided", () => { + const result = getCallbackUrl(undefined, "test-source"); + expect(result).toBe("/?source=test-source"); + }); + + it("should append source parameter to inviteUrl with existing query parameters", () => { + const result = getCallbackUrl("https://example.com/invite?param=value", "test-source"); + expect(result).toBe("https://example.com/invite?param=value&source=test-source"); + }); + + it("should append source parameter to inviteUrl without existing query parameters", () => { + const result = getCallbackUrl("https://example.com/invite", "test-source"); + expect(result).toBe("https://example.com/invite?source=test-source"); + }); + + it("should handle empty source parameter", () => { + const result = getCallbackUrl("https://example.com/invite", ""); + expect(result).toBe("https://example.com/invite?source="); + }); + + it("should handle undefined source parameter", () => { + const result = getCallbackUrl("https://example.com/invite", undefined); + expect(result).toBe("https://example.com/invite?source=undefined"); + }); +}); diff --git a/apps/web/modules/ee/sso/lib/utils.ts b/apps/web/modules/ee/sso/lib/utils.ts new file mode 100644 index 0000000000..a0f9bef776 --- /dev/null +++ b/apps/web/modules/ee/sso/lib/utils.ts @@ -0,0 +1,5 @@ +export const getCallbackUrl = (inviteUrl?: string, source?: string) => { + return inviteUrl + ? `${inviteUrl}${inviteUrl.includes("?") ? "&" : "?"}source=${source}` + : `/?source=${source}`; +}; diff --git a/apps/web/modules/survey/hooks/useSingleUseId.test.tsx b/apps/web/modules/survey/hooks/useSingleUseId.test.tsx index 206b5d01d1..e429ac2ca1 100644 --- a/apps/web/modules/survey/hooks/useSingleUseId.test.tsx +++ b/apps/web/modules/survey/hooks/useSingleUseId.test.tsx @@ -8,7 +8,7 @@ import { useSingleUseId } from "./useSingleUseId"; // Mock external functions vi.mock("@/modules/survey/list/actions", () => ({ - generateSingleUseIdAction: vi.fn(), + generateSingleUseIdAction: vi.fn().mockResolvedValue({ data: "initialId" }), })); vi.mock("@/lib/utils/helper", () => ({ @@ -88,8 +88,10 @@ describe("useSingleUseId", () => { vi.mocked(generateSingleUseIdAction).mockResolvedValueOnce({ data: "initialId" }); const { result } = renderHook(() => useSingleUseId(mockSurvey)); - // Wait for initial - await new Promise((r) => setTimeout(r, 0)); + // Wait for initial value to be set + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); expect(result.current.singleUseId).toBe("initialId"); vi.mocked(generateSingleUseIdAction).mockResolvedValueOnce({ data: "refreshedId" }); diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts index aa6fe1497d..6fa694f853 100644 --- a/apps/web/vite.config.mts +++ b/apps/web/vite.config.mts @@ -19,7 +19,8 @@ export default defineConfig({ "modules/api/v2/**/*.ts", "modules/api/v2/**/*.tsx", "modules/auth/lib/**/*.ts", - "modules/signup/lib/**/*.ts", + "modules/auth/signup/lib/**/*.ts", + "modules/auth/signup/**/*.tsx", "modules/ee/whitelabel/email-customization/components/*.tsx", "modules/ee/role-management/components/*.tsx", "modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx", @@ -52,6 +53,7 @@ export default defineConfig({ "modules/survey/list/components/survey-card.tsx", "modules/survey/list/components/survey-dropdown-menu.tsx", "modules/ee/contacts/api/v2/management/contacts/bulk/lib/contact.ts", + "modules/ee/sso/components/**/*.tsx", ], exclude: [ "**/.next/**", diff --git a/packages/lib/membership/service.ts b/packages/lib/membership/service.ts index 02a1544f50..2254371a81 100644 --- a/packages/lib/membership/service.ts +++ b/packages/lib/membership/service.ts @@ -54,6 +54,19 @@ export const createMembership = async ( validateInputs([organizationId, ZString], [userId, ZString], [data, ZMembership.partial()]); try { + const existingMembership = await prisma.membership.findUnique({ + where: { + userId_organizationId: { + userId, + organizationId, + }, + }, + }); + + if (existingMembership) { + return existingMembership; + } + const membership = await prisma.membership.create({ data: { userId, From 2c7f92a4d78b990fbc0b1f54fa29f3136e331d51 Mon Sep 17 00:00:00 2001 From: victorvhs017 <115753265+victorvhs017@users.noreply.github.com> Date: Sun, 6 Apr 2025 03:06:18 -0300 Subject: [PATCH 149/411] feat: user endpoints (#5232) Co-authored-by: Dhruwang Co-authored-by: pandeymangg --- .../[organizationId]/users/route.ts | 3 + apps/web/modules/api/v2/lib/response.ts | 2 +- .../modules/api/v2/management/lib/utils.ts | 3 +- .../responses/[responseId]/lib/openapi.ts | 8 +- .../responses/[responseId]/lib/response.ts | 4 +- .../responses/[responseId]/route.ts | 10 +- .../responses/[responseId]/types/responses.ts | 4 +- .../v2/management/responses/lib/openapi.ts | 2 +- .../v2/management/responses/lib/response.ts | 6 +- .../api/v2/management/responses/route.ts | 2 +- .../webhooks/[webhookId]/lib/openapi.ts | 8 +- .../[webhookId]/lib/tests/webhook.test.ts | 4 +- .../webhooks/[webhookId]/lib/webhook.ts | 4 +- .../management/webhooks/[webhookId]/route.ts | 12 +- .../webhooks/[webhookId]/types/webhooks.ts | 4 +- .../api/v2/management/webhooks/lib/openapi.ts | 2 +- .../api/v2/management/webhooks/lib/webhook.ts | 6 +- apps/web/modules/api/v2/me/route.ts | 9 + apps/web/modules/api/v2/openapi-document.ts | 8 + .../[organizationId]/lib/utils.test.ts | 4 +- .../[organizationId]/lib/utils.ts | 6 - .../project-teams/lib/openapi.ts | 6 +- .../project-teams/lib/project-teams.ts | 16 +- .../lib/tests/project-teams.test.ts | 6 +- .../[organizationId]/project-teams/route.ts | 14 +- .../project-teams/types/project-teams.ts | 2 +- .../[organizationId]/teams/lib/openapi.ts | 2 +- .../[organizationId]/teams/lib/teams.ts | 6 +- .../[organizationId]/teams/route.ts | 2 +- .../[organizationId]/users/lib/openapi.ts | 105 ++++ .../users/lib/tests/users.test.ts | 195 ++++++++ .../users/lib/tests/utils.test.ts | 45 ++ .../[organizationId]/users/lib/users.ts | 387 +++++++++++++++ .../[organizationId]/users/lib/utils.ts | 42 ++ .../[organizationId]/users/route.ts | 123 +++++ .../[organizationId]/users/types/users.ts | 42 ++ apps/web/playwright/api/constants.ts | 1 + .../api/organization/project-team.spec.ts | 4 +- .../playwright/api/organization/user.spec.ts | 131 +++++ docs/api-v2-reference/openapi.yml | 451 ++++++++++++++++-- packages/database/zod/users.ts | 88 ++++ sonar-project.properties | 4 +- 42 files changed, 1674 insertions(+), 109 deletions(-) create mode 100644 apps/web/app/api/v2/organizations/[organizationId]/users/route.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/users/lib/openapi.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/users/lib/utils.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts create mode 100644 apps/web/modules/api/v2/organizations/[organizationId]/users/types/users.ts create mode 100644 apps/web/playwright/api/organization/user.spec.ts create mode 100644 packages/database/zod/users.ts diff --git a/apps/web/app/api/v2/organizations/[organizationId]/users/route.ts b/apps/web/app/api/v2/organizations/[organizationId]/users/route.ts new file mode 100644 index 0000000000..1139ba0e5c --- /dev/null +++ b/apps/web/app/api/v2/organizations/[organizationId]/users/route.ts @@ -0,0 +1,3 @@ +import { GET, PATCH, POST } from "@/modules/api/v2/organizations/[organizationId]/users/route"; + +export { GET, POST, PATCH }; diff --git a/apps/web/modules/api/v2/lib/response.ts b/apps/web/modules/api/v2/lib/response.ts index ea4c51c92c..a378f75a36 100644 --- a/apps/web/modules/api/v2/lib/response.ts +++ b/apps/web/modules/api/v2/lib/response.ts @@ -235,7 +235,7 @@ const internalServerErrorResponse = ({ const successResponse = ({ data, meta, - cors = false, + cors = true, cache = "private, no-store", }: { data: Object; diff --git a/apps/web/modules/api/v2/management/lib/utils.ts b/apps/web/modules/api/v2/management/lib/utils.ts index bc10c929d7..105cda6122 100644 --- a/apps/web/modules/api/v2/management/lib/utils.ts +++ b/apps/web/modules/api/v2/management/lib/utils.ts @@ -13,7 +13,8 @@ type HasFindMany = | Prisma.WebhookFindManyArgs | Prisma.ResponseFindManyArgs | Prisma.TeamFindManyArgs - | Prisma.ProjectTeamFindManyArgs; + | Prisma.ProjectTeamFindManyArgs + | Prisma.UserFindManyArgs; export function buildCommonFilterQuery(query: T, params: TGetFilter): T { const { limit, skip, sortBy, order, startDate, endDate } = params || {}; diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/openapi.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/openapi.ts index de2ae256b9..c91a2fc836 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/openapi.ts @@ -1,4 +1,4 @@ -import { responseIdSchema } from "@/modules/api/v2/management/responses/[responseId]/types/responses"; +import { ZResponseIdSchema } from "@/modules/api/v2/management/responses/[responseId]/types/responses"; import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; import { z } from "zod"; import { ZodOpenApiOperationObject } from "zod-openapi"; @@ -11,7 +11,7 @@ export const getResponseEndpoint: ZodOpenApiOperationObject = { description: "Gets a response from the database.", requestParams: { path: z.object({ - id: responseIdSchema, + id: ZResponseIdSchema, }), }, tags: ["Management API > Responses"], @@ -34,7 +34,7 @@ export const deleteResponseEndpoint: ZodOpenApiOperationObject = { tags: ["Management API > Responses"], requestParams: { path: z.object({ - id: responseIdSchema, + id: ZResponseIdSchema, }), }, responses: { @@ -56,7 +56,7 @@ export const updateResponseEndpoint: ZodOpenApiOperationObject = { tags: ["Management API > Responses"], requestParams: { path: z.object({ - id: responseIdSchema, + id: ZResponseIdSchema, }), }, requestBody: { diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts index 2a4f0b8bab..a9d890fe28 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts @@ -1,7 +1,7 @@ import { deleteDisplay } from "@/modules/api/v2/management/responses/[responseId]/lib/display"; import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[responseId]/lib/survey"; import { findAndDeleteUploadedFilesInResponse } from "@/modules/api/v2/management/responses/[responseId]/lib/utils"; -import { responseUpdateSchema } from "@/modules/api/v2/management/responses/[responseId]/types/responses"; +import { ZResponseUpdateSchema } from "@/modules/api/v2/management/responses/[responseId]/types/responses"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Response } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; @@ -98,7 +98,7 @@ export const deleteResponse = async (responseId: string): Promise + responseInput: z.infer ): Promise> => { try { const updatedResponse = await prisma.response.update({ diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts index d9c6916e62..f71b66d7b1 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts @@ -9,13 +9,13 @@ import { } from "@/modules/api/v2/management/responses/[responseId]/lib/response"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { z } from "zod"; -import { responseIdSchema, responseUpdateSchema } from "./types/responses"; +import { ZResponseIdSchema, ZResponseUpdateSchema } from "./types/responses"; export const GET = async (request: Request, props: { params: Promise<{ responseId: string }> }) => authenticatedApiClient({ request, schemas: { - params: z.object({ responseId: responseIdSchema }), + params: z.object({ responseId: ZResponseIdSchema }), }, externalParams: props.params, handler: async ({ authentication, parsedInput }) => { @@ -52,7 +52,7 @@ export const DELETE = async (request: Request, props: { params: Promise<{ respon authenticatedApiClient({ request, schemas: { - params: z.object({ responseId: responseIdSchema }), + params: z.object({ responseId: ZResponseIdSchema }), }, externalParams: props.params, handler: async ({ authentication, parsedInput }) => { @@ -91,8 +91,8 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str request, externalParams: props.params, schemas: { - params: z.object({ responseId: responseIdSchema }), - body: responseUpdateSchema, + params: z.object({ responseId: ZResponseIdSchema }), + body: ZResponseUpdateSchema, }, handler: async ({ authentication, parsedInput }) => { const { body, params } = parsedInput; diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/types/responses.ts b/apps/web/modules/api/v2/management/responses/[responseId]/types/responses.ts index 68118bdbe2..8115c028b3 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/types/responses.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/types/responses.ts @@ -4,7 +4,7 @@ import { ZResponse } from "@formbricks/database/zod/responses"; extendZodWithOpenApi(z); -export const responseIdSchema = z +export const ZResponseIdSchema = z .string() .cuid2() .openapi({ @@ -16,7 +16,7 @@ export const responseIdSchema = z }, }); -export const responseUpdateSchema = ZResponse.omit({ +export const ZResponseUpdateSchema = ZResponse.omit({ id: true, surveyId: true, }).openapi({ diff --git a/apps/web/modules/api/v2/management/responses/lib/openapi.ts b/apps/web/modules/api/v2/management/responses/lib/openapi.ts index eb0dba284b..b1529cfaac 100644 --- a/apps/web/modules/api/v2/management/responses/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/responses/lib/openapi.ts @@ -13,7 +13,7 @@ export const getResponsesEndpoint: ZodOpenApiOperationObject = { summary: "Get responses", description: "Gets responses from the database.", requestParams: { - query: ZGetResponsesFilter.sourceType().required(), + query: ZGetResponsesFilter.sourceType(), }, tags: ["Management API > Responses"], responses: { diff --git a/apps/web/modules/api/v2/management/responses/lib/response.ts b/apps/web/modules/api/v2/management/responses/lib/response.ts index 5ba79f8408..2eb80bf9ed 100644 --- a/apps/web/modules/api/v2/management/responses/lib/response.ts +++ b/apps/web/modules/api/v2/management/responses/lib/response.ts @@ -134,12 +134,14 @@ export const getResponses = async ( params: TGetResponsesFilter ): Promise, ApiErrorResponseV2>> => { try { + const query = getResponsesQuery(environmentIds, params); + const [responses, count] = await prisma.$transaction([ prisma.response.findMany({ - ...getResponsesQuery(environmentIds, params), + ...query, }), prisma.response.count({ - where: getResponsesQuery(environmentIds, params).where, + where: query.where, }), ]); diff --git a/apps/web/modules/api/v2/management/responses/route.ts b/apps/web/modules/api/v2/management/responses/route.ts index 3dadae5a75..43961806ec 100644 --- a/apps/web/modules/api/v2/management/responses/route.ts +++ b/apps/web/modules/api/v2/management/responses/route.ts @@ -81,6 +81,6 @@ export const POST = async (request: Request) => return handleApiError(request, createResponseResult.error); } - return responses.successResponse({ data: createResponseResult.data, cors: true }); + return responses.successResponse({ data: createResponseResult.data }); }, }); diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts index 0c5d5cb3d2..43eb2ce696 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/openapi.ts @@ -1,4 +1,4 @@ -import { webhookIdSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; +import { ZWebhookIdSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; import { ZWebhookInput } from "@/modules/api/v2/management/webhooks/types/webhooks"; import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; import { z } from "zod"; @@ -11,7 +11,7 @@ export const getWebhookEndpoint: ZodOpenApiOperationObject = { description: "Gets a webhook from the database.", requestParams: { path: z.object({ - id: webhookIdSchema, + id: ZWebhookIdSchema, }), }, tags: ["Management API > Webhooks"], @@ -34,7 +34,7 @@ export const deleteWebhookEndpoint: ZodOpenApiOperationObject = { tags: ["Management API > Webhooks"], requestParams: { path: z.object({ - id: webhookIdSchema, + id: ZWebhookIdSchema, }), }, responses: { @@ -56,7 +56,7 @@ export const updateWebhookEndpoint: ZodOpenApiOperationObject = { tags: ["Management API > Webhooks"], requestParams: { path: z.object({ - id: webhookIdSchema, + id: ZWebhookIdSchema, }), }, requestBody: { diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/tests/webhook.test.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/tests/webhook.test.ts index 858f7fc74c..192685577a 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/tests/webhook.test.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/tests/webhook.test.ts @@ -3,7 +3,7 @@ import { mockedPrismaWebhookUpdateReturn, prismaNotFoundError, } from "@/modules/api/v2/management/webhooks/[webhookId]/lib/tests/mocks/webhook.mock"; -import { webhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; +import { ZWebhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; import { describe, expect, test, vi } from "vitest"; import { z } from "zod"; import { prisma } from "@formbricks/database"; @@ -61,7 +61,7 @@ describe("getWebhook", () => { }); describe("updateWebhook", () => { - const mockedWebhookUpdateReturn = { url: "https://example.com" } as z.infer; + const mockedWebhookUpdateReturn = { url: "https://example.com" } as z.infer; test("returns ok on successful update", async () => { vi.mocked(prisma.webhook.update).mockResolvedValueOnce(mockedPrismaWebhookUpdateReturn); diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts index 519cc3a9a7..a11645713e 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/lib/webhook.ts @@ -1,5 +1,5 @@ import { webhookCache } from "@/lib/cache/webhook"; -import { webhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; +import { ZWebhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { Webhook } from "@prisma/client"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; @@ -42,7 +42,7 @@ export const getWebhook = async (webhookId: string) => export const updateWebhook = async ( webhookId: string, - webhookInput: z.infer + webhookInput: z.infer ): Promise> => { try { const updatedWebhook = await prisma.webhook.update({ diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts index 70c810cdf1..1ec1d152b5 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/route.ts @@ -8,8 +8,8 @@ import { updateWebhook, } from "@/modules/api/v2/management/webhooks/[webhookId]/lib/webhook"; import { - webhookIdSchema, - webhookUpdateSchema, + ZWebhookIdSchema, + ZWebhookUpdateSchema, } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; import { NextRequest } from "next/server"; @@ -19,7 +19,7 @@ export const GET = async (request: NextRequest, props: { params: Promise<{ webho authenticatedApiClient({ request, schemas: { - params: z.object({ webhookId: webhookIdSchema }), + params: z.object({ webhookId: ZWebhookIdSchema }), }, externalParams: props.params, handler: async ({ authentication, parsedInput }) => { @@ -53,8 +53,8 @@ export const PUT = async (request: NextRequest, props: { params: Promise<{ webho authenticatedApiClient({ request, schemas: { - params: z.object({ webhookId: webhookIdSchema }), - body: webhookUpdateSchema, + params: z.object({ webhookId: ZWebhookIdSchema }), + body: ZWebhookUpdateSchema, }, externalParams: props.params, handler: async ({ authentication, parsedInput }) => { @@ -112,7 +112,7 @@ export const DELETE = async (request: NextRequest, props: { params: Promise<{ we authenticatedApiClient({ request, schemas: { - params: z.object({ webhookId: webhookIdSchema }), + params: z.object({ webhookId: ZWebhookIdSchema }), }, externalParams: props.params, handler: async ({ authentication, parsedInput }) => { diff --git a/apps/web/modules/api/v2/management/webhooks/[webhookId]/types/webhooks.ts b/apps/web/modules/api/v2/management/webhooks/[webhookId]/types/webhooks.ts index 9bcc7a708a..82c178a808 100644 --- a/apps/web/modules/api/v2/management/webhooks/[webhookId]/types/webhooks.ts +++ b/apps/web/modules/api/v2/management/webhooks/[webhookId]/types/webhooks.ts @@ -4,7 +4,7 @@ import { ZWebhook } from "@formbricks/database/zod/webhooks"; extendZodWithOpenApi(z); -export const webhookIdSchema = z +export const ZWebhookIdSchema = z .string() .cuid2() .openapi({ @@ -16,7 +16,7 @@ export const webhookIdSchema = z }, }); -export const webhookUpdateSchema = ZWebhook.omit({ +export const ZWebhookUpdateSchema = ZWebhook.omit({ id: true, createdAt: true, updatedAt: true, diff --git a/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts b/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts index 3530e8230c..c60b1d5af6 100644 --- a/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/webhooks/lib/openapi.ts @@ -13,7 +13,7 @@ export const getWebhooksEndpoint: ZodOpenApiOperationObject = { summary: "Get webhooks", description: "Gets webhooks from the database.", requestParams: { - query: ZGetWebhooksFilter.sourceType().required(), + query: ZGetWebhooksFilter.sourceType(), }, tags: ["Management API > Webhooks"], responses: { diff --git a/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts b/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts index 1720fe5018..175c6660b8 100644 --- a/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts +++ b/apps/web/modules/api/v2/management/webhooks/lib/webhook.ts @@ -13,12 +13,14 @@ export const getWebhooks = async ( params: TGetWebhooksFilter ): Promise, ApiErrorResponseV2>> => { try { + const query = getWebhooksQuery(environmentIds, params); + const [webhooks, count] = await prisma.$transaction([ prisma.webhook.findMany({ - ...getWebhooksQuery(environmentIds, params), + ...query, }), prisma.webhook.count({ - where: getWebhooksQuery(environmentIds, params).where, + where: query.where, }), ]); diff --git a/apps/web/modules/api/v2/me/route.ts b/apps/web/modules/api/v2/me/route.ts index 93878ea6dd..e2d4896820 100644 --- a/apps/web/modules/api/v2/me/route.ts +++ b/apps/web/modules/api/v2/me/route.ts @@ -1,11 +1,20 @@ import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; import { NextRequest } from "next/server"; +import { OrganizationAccessType } from "@formbricks/types/api-key"; export const GET = async (request: NextRequest) => authenticatedApiClient({ request, handler: async ({ authentication }) => { + if (!authentication.organizationAccess?.accessControl?.[OrganizationAccessType.Read]) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + return responses.successResponse({ data: { environmentPermissions: authentication.environmentPermissions.map((permission) => ({ diff --git a/apps/web/modules/api/v2/openapi-document.ts b/apps/web/modules/api/v2/openapi-document.ts index 7e8d805e35..4e2b3173ca 100644 --- a/apps/web/modules/api/v2/openapi-document.ts +++ b/apps/web/modules/api/v2/openapi-document.ts @@ -7,6 +7,7 @@ import { webhookPaths } from "@/modules/api/v2/management/webhooks/lib/openapi"; import { mePaths } from "@/modules/api/v2/me/lib/openapi"; import { projectTeamPaths } from "@/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi"; import { teamPaths } from "@/modules/api/v2/organizations/[organizationId]/teams/lib/openapi"; +import { userPaths } from "@/modules/api/v2/organizations/[organizationId]/users/lib/openapi"; import { rolePaths } from "@/modules/api/v2/roles/lib/openapi"; import { bulkContactPaths } from "@/modules/ee/contacts/api/v2/management/contacts/bulk/lib/openapi"; import * as yaml from "yaml"; @@ -21,6 +22,7 @@ import { ZResponse } from "@formbricks/database/zod/responses"; import { ZRoles } from "@formbricks/database/zod/roles"; import { ZSurveyWithoutQuestionType } from "@formbricks/database/zod/surveys"; import { ZTeam } from "@formbricks/database/zod/teams"; +import { ZUser } from "@formbricks/database/zod/users"; import { ZWebhook } from "@formbricks/database/zod/webhooks"; extendZodWithOpenApi(z); @@ -44,6 +46,7 @@ const document = createDocument({ ...webhookPaths, ...teamPaths, ...projectTeamPaths, + ...userPaths, }, servers: [ { @@ -92,6 +95,10 @@ const document = createDocument({ name: "Organizations API > Project Teams", description: "Operations for managing project teams.", }, + { + name: "Organizations API > Users", + description: "Operations for managing users.", + }, ], components: { securitySchemes: { @@ -113,6 +120,7 @@ const document = createDocument({ webhook: ZWebhook, team: ZTeam, projectTeam: ZProjectTeam, + user: ZUser, }, }, security: [ diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts index 5a8167b6d9..d89131950b 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.test.ts @@ -16,7 +16,9 @@ describe("hasOrganizationIdAndAccess", () => { const result = hasOrganizationIdAndAccess("org1", authentication, "read" as OrganizationAccessType); expect(result).toBe(false); - expect(spyError).toHaveBeenCalledWith("Organization ID is missing from the authentication object"); + expect(spyError).toHaveBeenCalledWith( + "Organization ID from params does not match the authenticated organization ID" + ); }); it("should return false and log error if param organizationId does not match authentication organizationId", () => { diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts index 59d1016080..b68ac23d28 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/lib/utils.ts @@ -7,12 +7,6 @@ export const hasOrganizationIdAndAccess = ( authentication: TAuthenticationApiKey, accessType: OrganizationAccessType ): boolean => { - if (!authentication.organizationId) { - logger.error("Organization ID is missing from the authentication object"); - - return false; - } - if (paramOrganizationId !== authentication.organizationId) { logger.error("Organization ID from params does not match the authenticated organization ID"); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts index 8a8dc57353..283023aaf6 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/openapi.ts @@ -2,7 +2,6 @@ import { ZGetProjectTeamUpdateFilter, ZGetProjectTeamsFilter, ZProjectTeamInput, - projectTeamUpdateSchema, } from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; import { organizationServer } from "@/modules/api/v2/organizations/lib/openapi"; @@ -16,7 +15,7 @@ export const getProjectTeamsEndpoint: ZodOpenApiOperationObject = { summary: "Get project teams", description: "Gets projectTeams from the database.", requestParams: { - query: ZGetProjectTeamsFilter.sourceType().required(), + query: ZGetProjectTeamsFilter.sourceType(), path: z.object({ organizationId: ZOrganizationIdSchema, }), @@ -94,7 +93,6 @@ export const updateProjectTeamEndpoint: ZodOpenApiOperationObject = { description: "Updates a project team in the database.", tags: ["Organizations API > Project Teams"], requestParams: { - query: ZGetProjectTeamUpdateFilter.required(), path: z.object({ organizationId: ZOrganizationIdSchema, }), @@ -104,7 +102,7 @@ export const updateProjectTeamEndpoint: ZodOpenApiOperationObject = { description: "The project team to update", content: { "application/json": { - schema: projectTeamUpdateSchema, + schema: ZProjectTeamInput, }, }, }, diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts index 1a2bcee222..3c06e04237 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/project-teams.ts @@ -3,7 +3,7 @@ import { getProjectTeamsQuery } from "@/modules/api/v2/organizations/[organizati import { TGetProjectTeamsFilter, TProjectTeamInput, - projectTeamUpdateSchema, + ZProjectZTeamUpdateSchema, } from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; @@ -19,12 +19,14 @@ export const getProjectTeams = async ( params: TGetProjectTeamsFilter ): Promise, ApiErrorResponseV2>> => { try { + const query = getProjectTeamsQuery(organizationId, params); + const [projectTeams, count] = await prisma.$transaction([ prisma.projectTeam.findMany({ - ...getProjectTeamsQuery(organizationId, params), + ...query, }), prisma.projectTeam.count({ - where: getProjectTeamsQuery(organizationId, params).where, + where: query.where, }), ]); @@ -67,14 +69,14 @@ export const createProjectTeam = async ( return ok(projectTeam); } catch (error) { - return err({ type: "internal_server_error", details: [{ field: "projecteam", issue: error.message }] }); + return err({ type: "internal_server_error", details: [{ field: "projectTeam", issue: error.message }] }); } }; export const updateProjectTeam = async ( teamId: string, projectId: string, - teamInput: z.infer + teamInput: z.infer ): Promise> => { try { const updatedProjectTeam = await prisma.projectTeam.update({ @@ -97,7 +99,7 @@ export const updateProjectTeam = async ( return ok(updatedProjectTeam); } catch (error) { - return err({ type: "internal_server_error", details: [{ field: "projecteam", issue: error.message }] }); + return err({ type: "internal_server_error", details: [{ field: "projectTeam", issue: error.message }] }); } }; @@ -125,6 +127,6 @@ export const deleteProjectTeam = async ( return ok(deletedProjectTeam); } catch (error) { - return err({ type: "internal_server_error", details: [{ field: "projecteam", issue: error.message }] }); + return err({ type: "internal_server_error", details: [{ field: "projectTeam", issue: error.message }] }); } }; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts index 3ced4cf4ba..bf7c7dc4b6 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/lib/tests/project-teams.test.ts @@ -1,7 +1,7 @@ import { TGetProjectTeamsFilter, TProjectTeamInput, - projectTeamUpdateSchema, + ZProjectZTeamUpdateSchema, } from "@/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { TypeOf } from "zod"; @@ -87,7 +87,7 @@ describe("ProjectTeams Lib", () => { permission: "READ", }); const result = await updateProjectTeam("t1", "p1", { permission: "READ" } as unknown as TypeOf< - typeof projectTeamUpdateSchema + typeof ZProjectZTeamUpdateSchema >); expect(result.ok).toBe(true); if (result.ok) { @@ -98,7 +98,7 @@ describe("ProjectTeams Lib", () => { it("returns internal_server_error on error", async () => { (prisma.projectTeam.update as any).mockRejectedValueOnce(new Error("Update error")); const result = await updateProjectTeam("t1", "p1", { permission: "READ" } as unknown as TypeOf< - typeof projectTeamUpdateSchema + typeof ZProjectZTeamUpdateSchema >); expect(result.ok).toBe(false); if (!result.ok) { diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts index 07360dba80..15ced42242 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/route.ts @@ -16,7 +16,6 @@ import { ZGetProjectTeamUpdateFilter, ZGetProjectTeamsFilter, ZProjectTeamInput, - projectTeamUpdateSchema, } from "./types/project-teams"; export async function GET(request: Request, props: { params: Promise<{ organizationId: string }> }) { @@ -95,7 +94,7 @@ export async function POST(request: Request, props: { params: Promise<{ organiza return handleApiError(request, result.error); } - return responses.successResponse({ data: result.data, cors: true }); + return responses.successResponse({ data: result.data }); }, }); } @@ -104,13 +103,12 @@ export async function PUT(request: Request, props: { params: Promise<{ organizat return authenticatedApiClient({ request, schemas: { - body: projectTeamUpdateSchema, - query: ZGetProjectTeamUpdateFilter, + body: ZProjectTeamInput, params: z.object({ organizationId: ZOrganizationIdSchema }), }, externalParams: props.params, - handler: async ({ parsedInput: { query, body, params }, authentication }) => { - const { teamId, projectId } = query!; + handler: async ({ parsedInput: { body, params }, authentication }) => { + const { teamId, projectId } = body!; if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { return handleApiError(request, { @@ -130,7 +128,7 @@ export async function PUT(request: Request, props: { params: Promise<{ organizat return handleApiError(request, result.error); } - return responses.successResponse({ data: result.data, cors: true }); + return responses.successResponse({ data: result.data }); }, }); } @@ -164,7 +162,7 @@ export async function DELETE(request: Request, props: { params: Promise<{ organi return handleApiError(request, result.error); } - return responses.successResponse({ data: result.data, cors: true }); + return responses.successResponse({ data: result.data }); }, }); } diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts index 11bf7c4580..d852852bd7 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/project-teams/types/project-teams.ts @@ -32,6 +32,6 @@ export const ZGetProjectTeamUpdateFilter = z.object({ projectId: z.string().cuid2(), }); -export const projectTeamUpdateSchema = ZProjectTeam.pick({ +export const ZProjectZTeamUpdateSchema = ZProjectTeam.pick({ permission: true, }); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts index 443d71ea98..f92910e592 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/openapi.ts @@ -22,7 +22,7 @@ export const getTeamsEndpoint: ZodOpenApiOperationObject = { path: z.object({ organizationId: ZOrganizationIdSchema, }), - query: ZGetTeamsFilter.sourceType().required(), + query: ZGetTeamsFilter.sourceType(), }, tags: ["Organizations API > Teams"], responses: { diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts index db5812b3ef..c6653cdf84 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/lib/teams.ts @@ -48,12 +48,14 @@ export const getTeams = async ( params: TGetTeamsFilter ): Promise, ApiErrorResponseV2>> => { try { + const query = getTeamsQuery(organizationId, params); + const [teams, count] = await prisma.$transaction([ prisma.team.findMany({ - ...getTeamsQuery(organizationId, params), + ...query, }), prisma.team.count({ - where: getTeamsQuery(organizationId, params).where, + where: query.where, }), ]); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts index bf185277ac..44b61a41bf 100644 --- a/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts +++ b/apps/web/modules/api/v2/organizations/[organizationId]/teams/route.ts @@ -59,6 +59,6 @@ export const POST = async (request: Request, props: { params: Promise<{ organiza return handleApiError(request, createTeamResult.error); } - return responses.successResponse({ data: createTeamResult.data, cors: true }); + return responses.successResponse({ data: createTeamResult.data }); }, }); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/openapi.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/openapi.ts new file mode 100644 index 0000000000..1289dcf996 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/openapi.ts @@ -0,0 +1,105 @@ +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { + ZGetUsersFilter, + ZUserInput, + ZUserInputPatch, +} from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; +import { organizationServer } from "@/modules/api/v2/organizations/lib/openapi"; +import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/types/openapi-response"; +import { z } from "zod"; +import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; +import { ZUser } from "@formbricks/database/zod/users"; + +export const getUsersEndpoint: ZodOpenApiOperationObject = { + operationId: "getUsers", + summary: "Get users", + description: `Gets users from the database.
Only available for self-hosted Formbricks.`, + requestParams: { + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + query: ZGetUsersFilter.sourceType(), + }, + tags: ["Organizations API > Users"], + responses: { + "200": { + description: "Users retrieved successfully.", + content: { + "application/json": { + schema: responseWithMetaSchema(makePartialSchema(ZUser)), + }, + }, + }, + }, +}; + +export const createUserEndpoint: ZodOpenApiOperationObject = { + operationId: "createUser", + summary: "Create a user", + description: `Create a new user in the database.
Only available for self-hosted Formbricks.`, + requestParams: { + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + }, + tags: ["Organizations API > Users"], + requestBody: { + required: true, + description: "The user to create", + content: { + "application/json": { + schema: ZUserInput, + }, + }, + }, + responses: { + "201": { + description: "User created successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZUser), + }, + }, + }, + }, +}; + +export const updateUserEndpoint: ZodOpenApiOperationObject = { + operationId: "updateUser", + summary: "Update a user", + description: `Updates an existing user in the database.
Only available for self-hosted Formbricks.`, + requestParams: { + path: z.object({ + organizationId: ZOrganizationIdSchema, + }), + }, + tags: ["Organizations API > Users"], + requestBody: { + required: true, + description: "The user to update", + content: { + "application/json": { + schema: ZUserInputPatch, + }, + }, + }, + responses: { + "200": { + description: "User updated successfully.", + content: { + "application/json": { + schema: makePartialSchema(ZUser), + }, + }, + }, + }, +}; + +export const userPaths: ZodOpenApiPathsObject = { + "/{organizationId}/users": { + servers: organizationServer, + get: getUsersEndpoint, + post: createUserEndpoint, + patch: updateUserEndpoint, + }, +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts new file mode 100644 index 0000000000..c94fc944ed --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/users.test.ts @@ -0,0 +1,195 @@ +import { teamCache } from "@/lib/cache/team"; +import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; +import { describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { membershipCache } from "@formbricks/lib/membership/cache"; +import { userCache } from "@formbricks/lib/user/cache"; +import { createUser, getUsers, updateUser } from "../users"; + +const mockUser = { + id: "user123", + email: "test@example.com", + name: "Test User", + lastLoginAt: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + isActive: true, + role: "admin", + memberships: [{ organizationId: "org456", role: "admin" }], + teamUsers: [{ team: { name: "Test Team", id: "team123", projectTeams: [{ projectId: "proj789" }] } }], +}; + +vi.mock("@formbricks/database", () => ({ + prisma: { + user: { + findMany: vi.fn(), + count: vi.fn(), + findUnique: vi.fn(), + create: vi.fn(), + update: vi.fn(), + }, + team: { + findMany: vi.fn(), + }, + teamUser: { + create: vi.fn(), + delete: vi.fn(), + }, + $transaction: vi.fn(), + }, +})); + +vi.spyOn(membershipCache, "revalidate").mockImplementation(() => {}); +vi.spyOn(userCache, "revalidate").mockImplementation(() => {}); +vi.spyOn(teamCache, "revalidate").mockImplementation(() => {}); + +describe("Users Lib", () => { + describe("getUsers", () => { + it("returns users with meta on success", async () => { + const usersArray = [mockUser]; + (prisma.$transaction as any).mockResolvedValueOnce([usersArray, usersArray.length]); + const result = await getUsers("org456", { limit: 10, skip: 0 } as TGetUsersFilter); + expect(prisma.$transaction).toHaveBeenCalled(); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.data).toStrictEqual([ + { + id: mockUser.id, + email: mockUser.email, + name: mockUser.name, + lastLoginAt: expect.any(Date), + isActive: true, + role: mockUser.role, + teams: ["Test Team"], + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }, + ]); + } + }); + + it("returns internal_server_error if prisma fails", async () => { + (prisma.$transaction as any).mockRejectedValueOnce(new Error("Transaction error")); + const result = await getUsers("org456", { limit: 10, skip: 0 } as TGetUsersFilter); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("createUser", () => { + it("creates user and revalidates caches", async () => { + (prisma.user.create as any).mockResolvedValueOnce(mockUser); + const result = await createUser( + { name: "Test User", email: "test@example.com", role: "member" }, + "org456" + ); + expect(prisma.user.create).toHaveBeenCalled(); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.id).toBe(mockUser.id); + } + }); + + it("returns internal_server_error if creation fails", async () => { + (prisma.user.create as any).mockRejectedValueOnce(new Error("Create error")); + const result = await createUser({ name: "fail", email: "fail@example.com", role: "manager" }, "org456"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("updateUser", () => { + it("updates user and revalidates caches", async () => { + (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser); + (prisma.$transaction as any).mockResolvedValueOnce([{ ...mockUser, name: "Updated User" }]); + const result = await updateUser({ email: mockUser.email, name: "Updated User" }, "org456"); + expect(prisma.user.findUnique).toHaveBeenCalled(); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.name).toBe("Updated User"); + } + }); + + it("returns not_found if user doesn't exist", async () => { + (prisma.user.findUnique as any).mockResolvedValueOnce(null); + const result = await updateUser({ email: "unknown@example.com" }, "org456"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("not_found"); + } + }); + + it("returns internal_server_error if update fails", async () => { + (prisma.user.findUnique as any).mockResolvedValueOnce(mockUser); + (prisma.$transaction as any).mockRejectedValueOnce(new Error("Update error")); + const result = await updateUser({ email: mockUser.email }, "org456"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); + }); + + describe("createUser with teams", () => { + it("creates user with existing teams", async () => { + (prisma.team.findMany as any).mockResolvedValueOnce([ + { id: "team123", name: "MyTeam", projectTeams: [{ projectId: "proj789" }] }, + ]); + (prisma.user.create as any).mockResolvedValueOnce({ + ...mockUser, + teamUsers: [{ team: { id: "team123", name: "MyTeam" } }], + }); + + const result = await createUser( + { name: "Test", email: "team@example.com", role: "manager", teams: ["MyTeam"], isActive: true }, + "org456" + ); + + expect(prisma.user.create).toHaveBeenCalled(); + expect(teamCache.revalidate).toHaveBeenCalled(); + expect(membershipCache.revalidate).toHaveBeenCalled(); + expect(result.ok).toBe(true); + }); + }); + + describe("updateUser with team changes", () => { + it("removes a team and adds new team", async () => { + (prisma.user.findUnique as any).mockResolvedValueOnce({ + ...mockUser, + teamUsers: [{ team: { id: "team123", name: "OldTeam", projectTeams: [{ projectId: "proj789" }] } }], + }); + (prisma.team.findMany as any).mockResolvedValueOnce([ + { id: "team456", name: "NewTeam", projectTeams: [] }, + ]); + (prisma.$transaction as any).mockResolvedValueOnce([ + // deleted OldTeam from user + { team: { id: "team123", name: "OldTeam", projectTeams: [{ projectId: "proj789" }] } }, + // created teamUsers for NewTeam + { + team: { id: "team456", name: "NewTeam", projectTeams: [] }, + }, + // updated user + { ...mockUser, name: "Updated Name" }, + ]); + + const result = await updateUser( + { email: mockUser.email, name: "Updated Name", teams: ["NewTeam"] }, + "org456" + ); + + expect(prisma.user.findUnique).toHaveBeenCalled(); + expect(teamCache.revalidate).toHaveBeenCalledTimes(3); + expect(membershipCache.revalidate).toHaveBeenCalled(); + expect(userCache.revalidate).toHaveBeenCalled(); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.teams).toContain("NewTeam"); + expect(result.data.name).toBe("Updated Name"); + } + }); + }); +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts new file mode 100644 index 0000000000..dd3cb07a2c --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/tests/utils.test.ts @@ -0,0 +1,45 @@ +import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; +import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; +import { describe, expect, it, vi } from "vitest"; +import { getUsersQuery } from "../utils"; + +vi.mock("@/modules/api/v2/management/lib/utils", () => ({ + pickCommonFilter: vi.fn(), + buildCommonFilterQuery: vi.fn(), +})); + +describe("getUsersQuery", () => { + it("returns default query if no params are provided", () => { + const result = getUsersQuery("org123"); + expect(result).toEqual({ + where: { + memberships: { + some: { + organizationId: "org123", + }, + }, + }, + }); + }); + + it("includes email filter if email param is provided", () => { + const result = getUsersQuery("org123", { email: "test@example.com" } as TGetUsersFilter); + expect(result.where?.email).toEqual({ + contains: "test@example.com", + mode: "insensitive", + }); + }); + + it("includes id filter if id param is provided", () => { + const result = getUsersQuery("org123", { id: "user123" } as TGetUsersFilter); + expect(result.where?.id).toBe("user123"); + }); + + it("applies baseFilter if pickCommonFilter returns something", () => { + vi.mocked(pickCommonFilter).mockReturnValueOnce({ someField: "test" } as unknown as ReturnType< + typeof pickCommonFilter + >); + getUsersQuery("org123", {} as TGetUsersFilter); + expect(buildCommonFilterQuery).toHaveBeenCalled(); + }); +}); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts new file mode 100644 index 0000000000..85b7aac577 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/users.ts @@ -0,0 +1,387 @@ +import { teamCache } from "@/lib/cache/team"; +import { getUsersQuery } from "@/modules/api/v2/organizations/[organizationId]/users/lib/utils"; +import { + TGetUsersFilter, + TUserInput, + TUserInputPatch, +} from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { ApiResponseWithMeta } from "@/modules/api/v2/types/api-success"; +import { OrganizationRole, Prisma, TeamUserRole } from "@prisma/client"; +import { prisma } from "@formbricks/database"; +import { TUser } from "@formbricks/database/zod/users"; +import { membershipCache } from "@formbricks/lib/membership/cache"; +import { captureTelemetry } from "@formbricks/lib/telemetry"; +import { userCache } from "@formbricks/lib/user/cache"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getUsers = async ( + organizationId: string, + params: TGetUsersFilter +): Promise, ApiErrorResponseV2>> => { + try { + const query = getUsersQuery(organizationId, params); + + const [users, count] = await prisma.$transaction([ + prisma.user.findMany({ + ...query, + include: { + teamUsers: { + include: { + team: true, + }, + }, + memberships: { + select: { + role: true, + organizationId: true, + }, + }, + }, + }), + prisma.user.count({ + where: query.where, + }), + ]); + + const returnedUsers = users.map( + (user) => + ({ + id: user.id, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + email: user.email, + name: user.name, + lastLoginAt: user.lastLoginAt, + isActive: user.isActive, + role: user.memberships.filter((membership) => membership.organizationId === organizationId)[0].role, + teams: user.teamUsers.map((teamUser) => teamUser.team.name), + }) as TUser + ); + + return ok({ + data: returnedUsers, + meta: { + total: count, + limit: params.limit, + offset: params.skip, + }, + }); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "users", issue: error.message }] }); + } +}; + +export const createUser = async ( + userInput: TUserInput, + organizationId +): Promise> => { + captureTelemetry("user created"); + + const { name, email, role, teams, isActive } = userInput; + + try { + const existingTeams = teams && (await getExistingTeamsFromInput(teams, organizationId)); + + let teamUsersToCreate; + + if (existingTeams) { + teamUsersToCreate = existingTeams.map((team) => ({ + role: TeamUserRole.contributor, + team: { + connect: { + id: team.id, + }, + }, + })); + } + + const prismaData: Prisma.UserCreateInput = { + name, + email, + isActive: isActive, + memberships: { + create: { + accepted: true, // auto accept because there is no invite + role: role.toLowerCase() as OrganizationRole, + organization: { + connect: { + id: organizationId, + }, + }, + }, + }, + teamUsers: + existingTeams?.length > 0 + ? { + create: teamUsersToCreate, + } + : undefined, + }; + + const user = await prisma.user.create({ + data: prismaData, + include: { + memberships: { + select: { + role: true, + organizationId: true, + }, + }, + }, + }); + + existingTeams?.forEach((team) => { + teamCache.revalidate({ + id: team.id, + organizationId: organizationId, + }); + + for (const projectTeam of team.projectTeams) { + teamCache.revalidate({ + projectId: projectTeam.projectId, + }); + } + }); + + // revalidate membership cache + membershipCache.revalidate({ + organizationId: organizationId, + userId: user.id, + }); + + const returnedUser = { + id: user.id, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + email: user.email, + name: user.name, + lastLoginAt: user.lastLoginAt, + isActive: user.isActive, + role: user.memberships.filter((membership) => membership.organizationId === organizationId)[0].role, + teams: existingTeams ? existingTeams.map((team) => team.name) : [], + } as TUser; + + return ok(returnedUser); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "user", issue: error.message }] }); + } +}; + +export const updateUser = async ( + userInput: TUserInputPatch, + organizationId: string +): Promise> => { + captureTelemetry("user updated"); + + const { name, email, role, teams, isActive } = userInput; + let existingTeams: string[] = []; + let newTeams; + + try { + // First, fetch the existing user along with memberships and teamUsers. + const existingUser = await prisma.user.findUnique({ + where: { email }, + include: { + memberships: { + select: { + role: true, + organizationId: true, + }, + }, + teamUsers: { + include: { + team: true, + }, + }, + }, + }); + + if (!existingUser) { + return err({ + type: "not_found", + details: [{ field: "user", issue: "not found" }], + }); + } + + // Capture the existing team names for the user. + existingTeams = existingUser.teamUsers.map((teamUser) => teamUser.team.name); + + // Build an array of operations for deleting teamUsers that are not in the input. + const deleteTeamOps = [] as Prisma.PrismaPromise[]; + existingUser.teamUsers.forEach((teamUser) => { + if (teams && !teams?.includes(teamUser.team.name)) { + deleteTeamOps.push( + prisma.teamUser.delete({ + where: { + teamId_userId: { + teamId: teamUser.team.id, + userId: existingUser.id, + }, + }, + include: { + team: { + include: { + projectTeams: { + select: { projectId: true }, + }, + }, + }, + }, + }) + ); + } + }); + + // Look up teams from the input that exist in this organization. + newTeams = await getExistingTeamsFromInput(teams, organizationId); + const existingUserTeamNames = existingUser.teamUsers.map((teamUser) => teamUser.team.name); + + // Build an array of operations for creating new teamUsers. + const createTeamOps = [] as Prisma.PrismaPromise[]; + newTeams?.forEach((team) => { + if (!existingUserTeamNames.includes(team.name)) { + createTeamOps.push( + prisma.teamUser.create({ + data: { + role: TeamUserRole.contributor, + user: { connect: { id: existingUser.id } }, + team: { connect: { id: team.id } }, + }, + include: { + team: { + include: { + projectTeams: { + select: { projectId: true }, + }, + }, + }, + }, + }) + ); + } + }); + + const prismaData: Prisma.UserUpdateInput = { + name: name ?? undefined, + email: email ?? undefined, + isActive: isActive ?? undefined, + memberships: { + updateMany: { + where: { + organizationId, + }, + data: { + role: role ? (role.toLowerCase() as OrganizationRole) : undefined, + }, + }, + }, + }; + + // Build the user update operation. + const updateUserOp = prisma.user.update({ + where: { email }, + data: prismaData, + include: { + memberships: { + select: { role: true, organizationId: true }, + }, + }, + }); + + // Combine all operations into one transaction. + const operations = [...deleteTeamOps, ...createTeamOps, updateUserOp]; + + // Execute the transaction. The result will be an array with the results in the same order. + const results = await prisma.$transaction(operations); + + // Retrieve the updated user result. Since the update was the last operation, it is the last item. + const updatedUser = results[results.length - 1]; + + // For each deletion, revalidate the corresponding team and its project caches. + for (const opResult of results.slice(0, deleteTeamOps.length)) { + const deletedTeamUser = opResult; + teamCache.revalidate({ + id: deletedTeamUser.team.id, + userId: existingUser.id, + organizationId, + }); + + deletedTeamUser.team.projectTeams.forEach((projectTeam) => { + teamCache.revalidate({ + projectId: projectTeam.projectId, + }); + }); + } + // For each creation, do the same. + for (const opResult of results.slice(deleteTeamOps.length, deleteTeamOps.length + createTeamOps.length)) { + const newTeamUser = opResult; + teamCache.revalidate({ + id: newTeamUser.team.id, + userId: existingUser.id, + organizationId, + }); + + newTeamUser.team.projectTeams.forEach((projectTeam) => { + teamCache.revalidate({ + projectId: projectTeam.projectId, + }); + }); + } + + // Revalidate membership and user caches for the updated user. + membershipCache.revalidate({ + organizationId, + userId: updatedUser.id, + }); + userCache.revalidate({ + id: updatedUser.id, + email: updatedUser.email, + }); + + const returnedUser = { + id: updatedUser.id, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + email: updatedUser.email, + name: updatedUser.name, + lastLoginAt: updatedUser.lastLoginAt, + isActive: updatedUser.isActive, + role: updatedUser.memberships.find( + (m: { organizationId: string }) => m.organizationId === organizationId + )?.role, + teams: newTeams ? newTeams.map((team) => team.name) : existingTeams, + }; + + return ok(returnedUser); + } catch (error) { + return err({ + type: "internal_server_error", + details: [{ field: "user", issue: error.message }], + }); + } +}; + +const getExistingTeamsFromInput = async (userInputTeams: string[] | undefined, organizationId: string) => { + let existingTeams; + + if (userInputTeams) { + existingTeams = await prisma.team.findMany({ + where: { + name: { in: userInputTeams }, + organizationId, + }, + select: { + id: true, + name: true, + projectTeams: { + select: { + projectId: true, + }, + }, + }, + }); + } + + return existingTeams; +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/utils.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/utils.ts new file mode 100644 index 0000000000..f27ece8677 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/lib/utils.ts @@ -0,0 +1,42 @@ +import { buildCommonFilterQuery, pickCommonFilter } from "@/modules/api/v2/management/lib/utils"; +import { TGetUsersFilter } from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; +import { Prisma } from "@prisma/client"; + +export const getUsersQuery = (organizationId: string, params?: TGetUsersFilter) => { + let query: Prisma.UserFindManyArgs = { + where: { + memberships: { + some: { + organizationId, + }, + }, + }, + }; + + if (!params) return query; + + if (params.email) { + query.where = { + ...query.where, + email: { + contains: params.email, + mode: "insensitive", + }, + }; + } + + if (params.id) { + query.where = { + ...query.where, + id: params.id, + }; + } + + const baseFilter = pickCommonFilter(params); + + if (baseFilter) { + query = buildCommonFilterQuery(query, baseFilter); + } + + return query; +}; diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts new file mode 100644 index 0000000000..7097d2d56d --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/route.ts @@ -0,0 +1,123 @@ +import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; +import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; +import { hasOrganizationIdAndAccess } from "@/modules/api/v2/organizations/[organizationId]/lib/utils"; +import { ZOrganizationIdSchema } from "@/modules/api/v2/organizations/[organizationId]/types/organizations"; +import { + createUser, + getUsers, + updateUser, +} from "@/modules/api/v2/organizations/[organizationId]/users/lib/users"; +import { + ZGetUsersFilter, + ZUserInput, + ZUserInputPatch, +} from "@/modules/api/v2/organizations/[organizationId]/users/types/users"; +import { NextRequest } from "next/server"; +import { z } from "zod"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { OrganizationAccessType } from "@formbricks/types/api-key"; + +export const GET = async (request: NextRequest, props: { params: Promise<{ organizationId: string }> }) => + authenticatedApiClient({ + request, + schemas: { + query: ZGetUsersFilter.sourceType(), + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput: { query, params } }) => { + if (IS_FORMBRICKS_CLOUD) { + return handleApiError(request, { + type: "bad_request", + details: [{ field: "organizationId", issue: "This endpoint is not supported on Formbricks Cloud" }], + }); + } + + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Read)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const res = await getUsers(authentication.organizationId, query!); + + if (!res.ok) { + return handleApiError(request, res.error); + } + + return responses.successResponse(res.data); + }, + }); + +export const POST = async (request: Request, props: { params: Promise<{ organizationId: string }> }) => + authenticatedApiClient({ + request, + schemas: { + body: ZUserInput, + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput: { body, params } }) => { + if (IS_FORMBRICKS_CLOUD) { + return handleApiError(request, { + type: "bad_request", + details: [{ field: "organizationId", issue: "This endpoint is not supported on Formbricks Cloud" }], + }); + } + + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + const createUserResult = await createUser(body!, authentication.organizationId); + if (!createUserResult.ok) { + return handleApiError(request, createUserResult.error); + } + + return responses.successResponse({ data: createUserResult.data }); + }, + }); + +export const PATCH = async (request: Request, props: { params: Promise<{ organizationId: string }> }) => + authenticatedApiClient({ + request, + schemas: { + body: ZUserInputPatch, + params: z.object({ organizationId: ZOrganizationIdSchema }), + }, + externalParams: props.params, + handler: async ({ authentication, parsedInput: { body, params } }) => { + if (IS_FORMBRICKS_CLOUD) { + return handleApiError(request, { + type: "bad_request", + details: [{ field: "organizationId", issue: "This endpoint is not supported on Formbricks Cloud" }], + }); + } + + if (!hasOrganizationIdAndAccess(params!.organizationId, authentication, OrganizationAccessType.Write)) { + return handleApiError(request, { + type: "unauthorized", + details: [{ field: "organizationId", issue: "unauthorized" }], + }); + } + + if (!body?.email) { + return handleApiError(request, { + type: "bad_request", + details: [{ field: "email", issue: "Email is required" }], + }); + } + + const updateUserResult = await updateUser(body, authentication.organizationId); + if (!updateUserResult.ok) { + return handleApiError(request, updateUserResult.error); + } + + return responses.successResponse({ data: updateUserResult.data }); + }, + }); diff --git a/apps/web/modules/api/v2/organizations/[organizationId]/users/types/users.ts b/apps/web/modules/api/v2/organizations/[organizationId]/users/types/users.ts new file mode 100644 index 0000000000..17458716e6 --- /dev/null +++ b/apps/web/modules/api/v2/organizations/[organizationId]/users/types/users.ts @@ -0,0 +1,42 @@ +import { ZGetFilter } from "@/modules/api/v2/types/api-filter"; +import { z } from "zod"; +import { ZUser } from "@formbricks/database/zod/users"; +import { ZUserName } from "@formbricks/types/user"; + +export const ZGetUsersFilter = ZGetFilter.extend({ + id: z.string().optional(), + email: z.string().optional(), +}).refine( + (data) => { + if (data.startDate && data.endDate && data.startDate > data.endDate) { + return false; + } + return true; + }, + { + message: "startDate must be before endDate", + } +); + +export type TGetUsersFilter = z.infer; + +export const ZUserInput = ZUser.omit({ + id: true, + createdAt: true, + updatedAt: true, + lastLoginAt: true, +}).extend({ + isActive: ZUser.shape.isActive.optional(), +}); + +export type TUserInput = z.infer; + +export const ZUserInputPatch = ZUserInput.extend({ + // Override specific fields to be optional + name: ZUserName.optional(), + role: ZUser.shape.role.optional(), + teams: ZUser.shape.teams.optional(), + isActive: ZUser.shape.isActive.optional(), +}); + +export type TUserInputPatch = z.infer; diff --git a/apps/web/playwright/api/constants.ts b/apps/web/playwright/api/constants.ts index f46f162295..75f7d4de56 100644 --- a/apps/web/playwright/api/constants.ts +++ b/apps/web/playwright/api/constants.ts @@ -7,3 +7,4 @@ export const ME_API_URL = `/api/v2/me`; export const TEAMS_API_URL = (organizationId: string) => `/api/v2/organizations/${organizationId}/teams`; export const PROJECT_TEAMS_API_URL = (organizationId: string) => `/api/v2/organizations/${organizationId}/project-teams`; +export const USERS_API_URL = (organizationId: string) => `/api/v2/organizations/${organizationId}/users`; diff --git a/apps/web/playwright/api/organization/project-team.spec.ts b/apps/web/playwright/api/organization/project-team.spec.ts index ebf5ce6043..2aeb919743 100644 --- a/apps/web/playwright/api/organization/project-team.spec.ts +++ b/apps/web/playwright/api/organization/project-team.spec.ts @@ -90,15 +90,15 @@ test.describe("API Tests for ProjectTeams", () => { await test.step("Update ProjectTeam by ID via API", async () => { const body = { permission: "read", + teamId: teamId, + projectId: projectId, }; - const queryParams = { teamId: teamId, projectId: projectId }; const response = await request.put(`${PROJECT_TEAMS_API_URL(organizationId)}`, { headers: { "Content-Type": "application/json", "x-api-key": apiKey, }, data: body, - params: queryParams, }); expect(response.ok()).toBe(true); diff --git a/apps/web/playwright/api/organization/user.spec.ts b/apps/web/playwright/api/organization/user.spec.ts new file mode 100644 index 0000000000..e81858c1fa --- /dev/null +++ b/apps/web/playwright/api/organization/user.spec.ts @@ -0,0 +1,131 @@ +import { ME_API_URL, TEAMS_API_URL, USERS_API_URL } from "@/playwright/api/constants"; +import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; +import { test } from "../../lib/fixtures"; +import { loginAndGetApiKey } from "../../lib/utils"; + +test.describe("API Tests for Users", () => { + test("Create, Retrieve, Filter, and Update Users via API", async ({ page, users, request }) => { + let apiKey; + let organizationId: string; + let createdUserId: string; + let teamName = "New Team from API"; + + const randomSuffix = Math.random().toString(36).substring(2, 15); + const userEmail = `usere2etest${randomSuffix}@formbricks-test.com`; + + try { + ({ apiKey } = await loginAndGetApiKey(page, users)); + } catch (error) { + logger.error(error, "Error during login and getting API key"); + throw error; + } + + await test.step("Get Organization ID", async () => { + const response = await request.get(ME_API_URL, { + headers: { "x-api-key": apiKey }, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data?.organizationId).toBeTruthy(); + organizationId = responseBody.data.organizationId; + }); + + // Create a team to use for the project team + await test.step("Create Team via API", async () => { + const teamBody = { + organizationId: organizationId, + name: teamName, + }; + + const response = await request.post(TEAMS_API_URL(organizationId), { + headers: { + "Content-Type": "application/json", + "x-api-key": apiKey, + }, + data: teamBody, + }); + + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.name).toEqual(teamName); + }); + + await test.step("Create User via API", async () => { + const userData = { + name: "E2E Test User", + email: userEmail, + role: "manager", + isActive: true, + teams: [teamName], + }; + + const response = await request.post(USERS_API_URL(organizationId), { + headers: { + "x-api-key": apiKey, + "Content-Type": "application/json", + }, + data: userData, + }); + + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.name).toEqual("E2E Test User"); + createdUserId = responseBody.data.id; + }); + + await test.step("Retrieve All Users via API", async () => { + const response = await request.get(USERS_API_URL(organizationId), { + headers: { "x-api-key": apiKey }, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(Array.isArray(responseBody.data)).toBe(true); + expect(responseBody.data.some((user: any) => user.id === createdUserId)).toBe(true); + }); + + await test.step("Filter Users by Email via API", async () => { + const queryParams = { email: userEmail }; + const response = await request.get(USERS_API_URL(organizationId), { + headers: { "x-api-key": apiKey }, + params: queryParams, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + + expect(responseBody.data.length).toBeGreaterThan(0); + expect(responseBody.data[0].email).toBe(userEmail); + }); + + await test.step("Partially Update User via PATCH", async () => { + const patchData = { email: userEmail, name: "Updated E2E Name" }; + const response = await request.patch(USERS_API_URL(organizationId), { + headers: { "x-api-key": apiKey, "Content-Type": "application/json" }, + data: patchData, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.name).toBe("Updated E2E Name"); + }); + + await test.step("Fully Update User via PATCH", async () => { + const patchData = { + email: userEmail, + name: "Fully Updated E2E", + role: "member", + teams: [], + isActive: false, + }; + const response = await request.patch(USERS_API_URL(organizationId), { + headers: { "x-api-key": apiKey, "Content-Type": "application/json" }, + data: patchData, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + expect(responseBody.data.name).toBe("Fully Updated E2E"); + expect(responseBody.data.role).toBe("member"); + expect(responseBody.data.isActive).toBe(false); + expect(responseBody.data.teams).toEqual([]); + }); + }); +}); diff --git a/docs/api-v2-reference/openapi.yml b/docs/api-v2-reference/openapi.yml index e448d7f68a..af3f71f47a 100644 --- a/docs/api-v2-reference/openapi.yml +++ b/docs/api-v2-reference/openapi.yml @@ -27,6 +27,8 @@ tags: description: Operations for managing teams. - name: Organizations API > Project Teams description: Operations for managing project teams. + - name: Organizations API > Users + description: Operations for managing users. security: - apiKeyAuth: [] paths: @@ -547,7 +549,15 @@ paths: data: type: array items: - type: string + anyOf: + - type: string + const: owner + - type: string + const: manager + - type: string + const: member + - type: string + const: billing /me: get: operationId: me @@ -650,22 +660,18 @@ paths: name: startDate schema: type: string - required: true - in: query name: endDate schema: type: string - required: true - in: query name: surveyId schema: type: string - required: true - in: query name: contactId schema: type: string - required: true responses: "200": description: Responses retrieved successfully. @@ -2264,19 +2270,16 @@ paths: name: startDate schema: type: string - required: true - in: query name: endDate schema: type: string - required: true - in: query name: surveyIds schema: type: array items: type: string - required: true responses: "200": description: Webhooks retrieved successfully. @@ -2704,12 +2707,10 @@ paths: name: startDate schema: type: string - required: true - in: query name: endDate schema: type: string - required: true responses: "200": description: Teams retrieved successfully. @@ -2998,22 +2999,18 @@ paths: name: startDate schema: type: string - required: true - in: query name: endDate schema: type: string - required: true - in: query name: teamId schema: type: string - required: true - in: query name: projectId schema: type: string - required: true responses: "200": description: Project teams retrieved successfully. @@ -3137,16 +3134,6 @@ paths: schema: $ref: "#/components/schemas/organizationId" required: true - - in: query - name: teamId - schema: - type: string - required: true - - in: query - name: projectId - schema: - type: string - required: true requestBody: required: true description: The project team to update @@ -3155,6 +3142,12 @@ paths: schema: type: object properties: + teamId: + type: string + description: The ID of the team + projectId: + type: string + description: The ID of the project permission: type: string enum: @@ -3163,6 +3156,8 @@ paths: - manage description: Level of access granted to the project required: + - teamId + - projectId - permission responses: "200": @@ -3245,6 +3240,334 @@ paths: - readWrite - manage description: Level of access granted to the project + /{organizationId}/users: + servers: *a10 + get: + operationId: getUsers + summary: Get users + description: Gets users from the database.
Only available for self-hosted + Formbricks. + tags: + - Organizations API > Users + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + - in: query + name: limit + schema: + type: number + minimum: 1 + maximum: 100 + default: 10 + - in: query + name: skip + schema: + type: number + minimum: 0 + default: 0 + - in: query + name: sortBy + schema: + type: string + enum: *a6 + default: createdAt + - in: query + name: order + schema: + type: string + enum: *a7 + default: desc + - in: query + name: startDate + schema: + type: string + - in: query + name: endDate + schema: + type: string + - in: query + name: id + schema: + type: string + - in: query + name: email + schema: + type: string + responses: + "200": + description: Users retrieved successfully. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + id: + type: string + description: The ID of the user + createdAt: + type: string + description: The date and time the user was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the user was last updated + example: 2021-01-01T00:00:00.000Z + lastLoginAt: + type: string + description: The date and time the user last logged in + example: 2021-01-01T00:00:00.000Z + isActive: + type: boolean + description: Whether the user is active + example: true + name: + type: string + pattern: ^[\p{L}\p{M}\s'\d-]+$ + minLength: 1 + description: The name of the user + example: John Doe + email: + type: string + format: email + maxLength: 255 + description: The email of the user + example: example@example.com + role: + type: string + enum: &a11 + - owner + - manager + - member + description: The role of the user in the organization + example: member + teams: + type: array + items: + type: string + description: The list of teams the user is a member of + example: &a12 + - team1 + - team2 + meta: + type: object + properties: + total: + type: number + limit: + type: number + offset: + type: number + post: + operationId: createUser + summary: Create a user + description: Create a new user in the database.
Only available for + self-hosted Formbricks. + tags: + - Organizations API > Users + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + requestBody: + required: true + description: The user to create + content: + application/json: + schema: + type: object + properties: + isActive: + type: boolean + description: Whether the user is active + example: true + name: + type: string + pattern: ^[\p{L}\p{M}\s'\d-]+$ + minLength: 1 + description: The name of the user + example: John Doe + email: + type: string + format: email + maxLength: 255 + description: The email of the user + example: example@example.com + role: + type: string + enum: *a11 + description: The role of the user in the organization + example: member + teams: + type: array + items: + type: string + description: The list of teams the user is a member of + example: *a12 + required: + - name + - email + - role + responses: + "201": + description: User created successfully. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: The ID of the user + createdAt: + type: string + description: The date and time the user was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the user was last updated + example: 2021-01-01T00:00:00.000Z + lastLoginAt: + type: string + description: The date and time the user last logged in + example: 2021-01-01T00:00:00.000Z + isActive: + type: boolean + description: Whether the user is active + example: true + name: + type: string + pattern: ^[\p{L}\p{M}\s'\d-]+$ + minLength: 1 + description: The name of the user + example: John Doe + email: + type: string + format: email + maxLength: 255 + description: The email of the user + example: example@example.com + role: + type: string + enum: *a11 + description: The role of the user in the organization + example: member + teams: + type: array + items: + type: string + description: The list of teams the user is a member of + example: *a12 + patch: + operationId: updateUser + summary: Update a user + description: Updates an existing user in the database.
Only available for + self-hosted Formbricks. + tags: + - Organizations API > Users + parameters: + - in: path + name: organizationId + description: The ID of the organization + schema: + $ref: "#/components/schemas/organizationId" + required: true + requestBody: + required: true + description: The user to update + content: + application/json: + schema: + type: object + properties: + isActive: + type: boolean + description: Whether the user is active + example: true + name: + type: string + pattern: ^[\p{L}\p{M}\s'\d-]+$ + minLength: 1 + email: + type: string + format: email + maxLength: 255 + description: The email of the user + example: example@example.com + role: + type: string + enum: *a11 + description: The role of the user in the organization + example: member + teams: + type: array + items: + type: string + description: The list of teams the user is a member of + example: *a12 + required: + - email + responses: + "200": + description: User updated successfully. + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: The ID of the user + createdAt: + type: string + description: The date and time the user was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the user was last updated + example: 2021-01-01T00:00:00.000Z + lastLoginAt: + type: string + description: The date and time the user last logged in + example: 2021-01-01T00:00:00.000Z + isActive: + type: boolean + description: Whether the user is active + example: true + name: + type: string + pattern: ^[\p{L}\p{M}\s'\d-]+$ + minLength: 1 + description: The name of the user + example: John Doe + email: + type: string + format: email + maxLength: 255 + description: The email of the user + example: example@example.com + role: + type: string + enum: *a11 + description: The role of the user in the organization + example: member + teams: + type: array + items: + type: string + description: The list of teams the user is a member of + example: *a12 components: securitySchemes: apiKeyAuth: @@ -3259,7 +3582,15 @@ components: data: type: array items: - type: string + anyOf: + - type: string + const: owner + - type: string + const: manager + - type: string + const: member + - type: string + const: billing required: - data me: @@ -3708,7 +4039,7 @@ components: required: - id - type - default: &a12 [] + default: &a14 [] description: The endings of the survey thankYouCard: type: @@ -3776,7 +4107,7 @@ components: description: Survey variables displayOption: type: string - enum: &a13 + enum: &a15 - displayOnce - displayMultiple - displaySome @@ -4011,13 +4342,13 @@ components: properties: linkSurveys: type: string - enum: &a11 + enum: &a13 - casual - straight - simple appSurveys: type: string - enum: *a11 + enum: *a13 required: - linkSurveys - appSurveys @@ -4248,6 +4579,60 @@ components: - projectId - teamId - permission + user: + type: object + properties: + id: + type: string + description: The ID of the user + createdAt: + type: string + description: The date and time the user was created + example: 2021-01-01T00:00:00.000Z + updatedAt: + type: string + description: The date and time the user was last updated + example: 2021-01-01T00:00:00.000Z + lastLoginAt: + type: string + description: The date and time the user last logged in + example: 2021-01-01T00:00:00.000Z + isActive: + type: boolean + description: Whether the user is active + example: true + name: + type: string + pattern: ^[\p{L}\p{M}\s'\d-]+$ + minLength: 1 + description: The name of the user + example: John Doe + email: + type: string + format: email + maxLength: 255 + description: The email of the user + example: example@example.com + role: + type: string + enum: *a11 + description: The role of the user in the organization + example: member + teams: + type: array + items: + type: string + description: The list of teams the user is a member of + example: *a12 + required: + - id + - createdAt + - updatedAt + - lastLoginAt + - isActive + - name + - email + - role responseId: type: string description: The ID of the response @@ -4393,7 +4778,7 @@ components: required: - id - type - default: *a12 + default: *a14 description: The endings of the survey thankYouCard: type: @@ -4459,7 +4844,7 @@ components: description: Survey variables displayOption: type: string - enum: *a13 + enum: *a15 description: Display options for the survey recontactDays: type: @@ -4719,10 +5104,10 @@ components: properties: linkSurveys: type: string - enum: *a11 + enum: *a13 appSurveys: type: string - enum: *a11 + enum: *a13 required: - linkSurveys - appSurveys diff --git a/packages/database/zod/users.ts b/packages/database/zod/users.ts new file mode 100644 index 0000000000..35d1d32785 --- /dev/null +++ b/packages/database/zod/users.ts @@ -0,0 +1,88 @@ +import { OrganizationRole, User } from "@prisma/client"; +import { z } from "zod"; +import { extendZodWithOpenApi } from "zod-openapi"; +import { ZUserEmail, ZUserName } from "../../types/user"; + +extendZodWithOpenApi(z); + +const ZNoBillingOrganizationRoles = z.enum( + Object.values(OrganizationRole).filter((role) => role !== OrganizationRole.billing) as [ + OrganizationRole, + ...OrganizationRole[], + ] +); + +export type TNoBillingOrganizationRoles = z.infer; + +export const ZUser = z.object({ + id: z.string().cuid2().openapi({ + description: "The ID of the user", + }), + createdAt: z.coerce.date().openapi({ + description: "The date and time the user was created", + example: "2021-01-01T00:00:00.000Z", + }), + updatedAt: z.coerce.date().openapi({ + description: "The date and time the user was last updated", + example: "2021-01-01T00:00:00.000Z", + }), + lastLoginAt: z.coerce.date().openapi({ + description: "The date and time the user last logged in", + example: "2021-01-01T00:00:00.000Z", + }), + isActive: z.boolean().openapi({ + description: "Whether the user is active", + example: true, + }), + name: ZUserName.openapi({ + description: "The name of the user", + example: "John Doe", + }), + email: ZUserEmail.openapi({ + description: "The email of the user", + example: "example@example.com", + }), + role: ZNoBillingOrganizationRoles.openapi({ + description: "The role of the user in the organization", + example: OrganizationRole.member, + }), + teams: z + .array(z.string()) + .optional() + .openapi({ + description: "The list of teams the user is a member of", + example: ["team1", "team2"], + }), +}) satisfies z.ZodType< + Omit< + User, + | "emailVerified" + | "imageUrl" + | "twoFactorSecret" + | "twoFactorEnabled" + | "backupCodes" + | "password" + | "identityProvider" + | "identityProviderAccountId" + | "memberships" + | "accounts" + | "responseNotes" + | "groupId" + | "invitesCreated" + | "invitesAccepted" + | "objective" + | "notificationSettings" + | "locale" + | "surveys" + | "teamUsers" + | "role" //doesn't satisfy the type because we remove the billing role + | "deprecatedRole" + > +>; + +ZUser.openapi({ + ref: "user", + description: "A user", +}); + +export type TUser = z.infer; diff --git a/sonar-project.properties b/sonar-project.properties index bcce9834f1..ff19bd4f85 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false sonar.sourceEncoding=UTF-8 # Coverage -sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/**,**/instrumentation.ts,scripts/merge-client-endpoints.ts -sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,modules/**/types/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts \ No newline at end of file +sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/*.stories.*,**/mocks/**,**/openapi.ts,**/openapi-document.ts,playwright/**,**/instrumentation.ts,scripts/merge-client-endpoints.ts +sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts \ No newline at end of file From 9802536ded784bd685eb75da1765f69af48ad384 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Mon, 7 Apr 2025 14:40:10 +0900 Subject: [PATCH 150/411] chore: upgrade demo app to tailwind v4 (#5237) Co-authored-by: Dhruwang --- apps/demo/components/sidebar.tsx | 8 +- apps/demo/globals.css | 26 ++++- apps/demo/package.json | 3 +- apps/demo/pages/index.tsx | 4 +- apps/demo/postcss.config.js | 3 +- apps/demo/tailwind.config.js | 13 --- pnpm-lock.yaml | 189 +++++++++++++++++++++++++++---- 7 files changed, 200 insertions(+), 46 deletions(-) delete mode 100644 apps/demo/tailwind.config.js diff --git a/apps/demo/components/sidebar.tsx b/apps/demo/components/sidebar.tsx index be9f3cd8ed..4e54a63d2a 100644 --- a/apps/demo/components/sidebar.tsx +++ b/apps/demo/components/sidebar.tsx @@ -27,7 +27,7 @@ const secondaryNavigation = [ export function Sidebar(): React.JSX.Element { return ( -
+