Compare commits

...

28 Commits

Author SHA1 Message Date
pandeymangg
2254c24cad fix: github action 2024-05-07 16:41:46 +05:30
pandeymangg
0f28cea7c8 fix: github action 2024-05-07 16:38:47 +05:30
pandeymangg
868ee71e34 fix: github action 2024-05-07 16:33:29 +05:30
pandeymangg
1dbbaca8fd fix: github action 2024-05-07 16:32:06 +05:30
pandeymangg
8b895b4b43 Merge branch 'main' into piyush/enterprise-license-check 2024-05-07 15:44:14 +05:30
pandeymangg
f81eae3691 fix: github action 2024-05-07 14:59:32 +05:30
pandeymangg
317013eee7 fix 2024-05-07 14:25:26 +05:30
pandeymangg
d19470f71a fix: e2e enbv 2024-05-07 13:56:21 +05:30
pandeymangg
3bf5693af1 fix: adds e2e check fallback for test environment 2024-05-07 13:30:56 +05:30
Matti Nannt
70cf58b55c Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 18:00:45 +02:00
Anshuman Pandey
68fc25587c Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 17:39:17 +05:30
pandeymangg
4c54c0d934 Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 17:20:49 +05:30
pandeymangg
93b49969f9 fix: removes logs, refactor 2024-05-06 17:18:43 +05:30
pandeymangg
2839e49ccb testing 2024-05-06 16:52:57 +05:30
pandeymangg
e1dae1bd98 Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 16:33:46 +05:30
pandeymangg
606305e54b Merge branch 'main' into piyush/enterprise-license-check 2024-05-03 15:55:45 +05:30
pandeymangg
0d1ded1139 Merge branch 'main' into piyush/enterprise-license-check 2024-05-03 14:06:20 +05:30
pandeymangg
6b1b2895f8 fix: feedback 2024-05-03 14:05:59 +05:30
pandeymangg
a3cb37b128 refactor 2024-05-02 15:06:07 +05:30
pandeymangg
b27314cec6 fix: caching for license key check 2024-05-02 15:03:58 +05:30
pandeymangg
1891f286e7 wip 2024-05-01 15:36:03 +05:30
pandeymangg
48409ced60 fix: short circuit logix 2024-05-01 11:41:36 +05:30
pandeymangg
9a7e5bfa8d Merge branch 'main' into piyush/enterprise-license-check 2024-05-01 11:36:23 +05:30
Piyush Gupta
adcba0139a Merge branch 'main' of https://github.com/formbricks/formbricks into piyush/enterprise-license-check 2024-04-16 09:53:19 +05:30
Piyush Gupta
0d3f7103af refactoring of getIsEnterpriseEdition service 2024-04-15 15:01:21 +05:30
Piyush Gupta
a51d68d23f Merge branch 'main' of https://github.com/formbricks/formbricks into piyush/enterprise-license-check 2024-04-15 09:09:21 +05:30
Piyush Gupta
0d33c27295 used unstable_cache instead of custom cache 2024-04-12 16:29:00 +05:30
Piyush Gupta
4fa4528771 adds enterprise license check 2024-04-12 15:02:07 +05:30
18 changed files with 170 additions and 39 deletions

View File

@@ -1,5 +1,13 @@
name: Build & Cache Web App
on:
workflow_dispatch:
inputs:
e2e_testing_mode:
description: "Set E2E Testing Mode"
required: false
default: "0"
runs:
using: "composite"
steps:
@@ -8,6 +16,9 @@ runs:
- uses: ./.github/actions/dangerous-git-checkout
- run: echo "E2E Testing Mode is ${{ inputs.e2e_testing_mode }}"
shell: bash
- name: Cache Build
uses: actions/cache@v3
id: cache-build
@@ -41,6 +52,11 @@ runs:
run: cp .env.example .env
shell: bash
- name: Add E2E Testing Mode
run: |
echo "E2E_TESTING=${{ inputs.e2e_testing_mode }}" >> $GITHUB_ENV
shell: bash
- name: Generate Random ENCRYPTION_KEY
run: |
SECRET=$(openssl rand -hex 32)

View File

@@ -2,6 +2,9 @@ name: E2E Tests
on:
workflow_call:
workflow_dispatch:
push:
branches:
- action-test
jobs:
build:
name: Run E2E Tests
@@ -13,6 +16,8 @@ jobs:
- name: Build & Cache Web Binaries
uses: ./.github/actions/cache-build-web
with:
e2e_testing_mode: "1"
- name: Install pnpm
uses: pnpm/action-setup@v2

View File

@@ -16,7 +16,7 @@ export default async function PeopleLayout({ params, children }) {
throw new Error("Team not found");
}
const isUserTargetingAllowed = getAdvancedTargetingPermission(team);
const isUserTargetingAllowed = await getAdvancedTargetingPermission(team);
return (
<>

View File

@@ -29,7 +29,7 @@ export default async function SegmentsPage({ params }) {
throw new Error("Team not found");
}
const isAdvancedTargetingAllowed = getAdvancedTargetingPermission(team);
const isAdvancedTargetingAllowed = await getAdvancedTargetingPermission(team);
if (!segments) {
throw new Error("Failed to fetch segments");

View File

@@ -25,7 +25,7 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env
return <ErrorComponent />;
}
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const [products, environments] = await Promise.all([
getProducts(team.id),

View File

@@ -37,7 +37,7 @@ export default async function EnterpriseLicensePage({ params }) {
notFound();
}
const isEnterpriseEdition = getIsEnterpriseEdition();
const isEnterpriseEdition = await getIsEnterpriseEdition();
const paidFeatures = [
{

View File

@@ -20,7 +20,7 @@ export default async function LanguageSettingsPage({ params }: { params: { envir
throw new Error("Team not found");
}
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
if (!isMultiLanguageAllowed) {
notFound();

View File

@@ -33,7 +33,7 @@ export default async function SettingsLayout({ children, params }) {
throw new Error("Unauthenticated");
}
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);

View File

@@ -23,7 +23,7 @@ export async function EditMemberships({
const currentUserRole = membership?.role;
const isUserAdminOrOwner = membership?.role === "admin" || membership?.role === "owner";
const canDoRoleManagement = getRoleManagementPermission(team);
const canDoRoleManagement = await getRoleManagementPermission(team);
return (
<div>

View File

@@ -51,7 +51,7 @@ export default async function MembersSettingsPage({ params }: { params: { enviro
if (!team) {
throw new Error("Team not found");
}
const canDoRoleManagement = getRoleManagementPermission(team);
const canDoRoleManagement = await getRoleManagementPermission(team);
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const { isOwner, isAdmin } = getAccessFlags(currentUserMembership?.role);

View File

@@ -59,8 +59,8 @@ export default async function SurveysEditPage({ params }) {
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isSurveyCreationDeletionDisabled = isViewer;
const isUserTargetingAllowed = getAdvancedTargetingPermission(team);
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
const isUserTargetingAllowed = await getAdvancedTargetingPermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
if (
!survey ||

View File

@@ -61,7 +61,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
if (!team) {
throw new Error("Team not found");
}
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
const isMultiLanguageAllowed = await getMultiLanguagePermission(team);
if (survey && survey.status !== "inProgress") {
return (

View File

@@ -1,13 +1,134 @@
import "server-only";
import { ENTERPRISE_LICENSE_KEY, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { cache, revalidateTag } from "@formbricks/lib/cache";
import { E2E_TESTING, ENTERPRISE_LICENSE_KEY, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { hashString } from "@formbricks/lib/hashString";
import { TTeam } from "@formbricks/types/teams";
export const getIsEnterpriseEdition = (): boolean => {
if (ENTERPRISE_LICENSE_KEY) {
return ENTERPRISE_LICENSE_KEY.length > 0;
import { prisma } from "../../database/src";
const PREVIOUS_RESULTS_CACHE_TAG = "getPreviousResult";
// This function is used to get the previous result of the license check from the cache
// This might seem confusing at first since we only return the default value from this function,
// but since we are using a cache and the cache key is the same, the cache will return the previous result - so this functions as a cache getter
const getPreviousResult = (): Promise<{ active: boolean | null; lastChecked: Date }> =>
cache(
async () => ({
active: null,
lastChecked: new Date(0),
}),
[PREVIOUS_RESULTS_CACHE_TAG],
{
tags: [PREVIOUS_RESULTS_CACHE_TAG],
}
)();
// This function is used to set the previous result of the license check to the cache so that we can use it in the next call
// Uses the same cache key as the getPreviousResult function
const setPreviousResult = async (previousResult: { active: boolean | null; lastChecked: Date }) => {
revalidateTag(PREVIOUS_RESULTS_CACHE_TAG);
const { lastChecked, active } = previousResult;
await cache(
async () => ({
active,
lastChecked,
}),
[PREVIOUS_RESULTS_CACHE_TAG],
{
tags: [PREVIOUS_RESULTS_CACHE_TAG],
}
)();
};
export const getIsEnterpriseEdition = async (): Promise<boolean> => {
if (!ENTERPRISE_LICENSE_KEY || ENTERPRISE_LICENSE_KEY.length === 0) {
return false;
}
const hashedKey = hashString(ENTERPRISE_LICENSE_KEY);
if (E2E_TESTING) {
const previousResult = await getPreviousResult();
if (previousResult.lastChecked.getTime() === new Date(0).getTime()) {
// first call
await setPreviousResult({ active: true, lastChecked: new Date() });
return true;
} else if (new Date().getTime() - previousResult.lastChecked.getTime() > 24 * 60 * 60 * 1000) {
// Fail after 24 hours
console.log("E2E_TESTING is enabled. Enterprise license was revoked after 24 hours.");
return false;
}
return previousResult.active !== null ? previousResult.active : false;
}
// if the server responds with a boolean, we return it
// if the server errors, we return null
// null signifies an error
const isValid: boolean | null = await cache(
async () => {
try {
const now = new Date();
const startOfYear = new Date(now.getFullYear(), 0, 1); // January 1st of the current year
const endOfYear = new Date(now.getFullYear() + 1, 0, 0); // December 31st of the current year
const responseCount = await prisma.response.count({
where: {
createdAt: {
gte: startOfYear,
lt: endOfYear,
},
},
});
const res = await fetch("https://ee.formbricks.com/api/licenses/check", {
body: JSON.stringify({
licenseKey: ENTERPRISE_LICENSE_KEY,
usage: { responseCount: responseCount },
}),
headers: { "Content-Type": "application/json" },
method: "POST",
});
if (res.ok) {
const responseJson = await res.json();
return responseJson.data.status === "active";
}
return null;
} catch (error) {
console.error("Error while checking license: ", error);
return null;
}
},
[`getIsEnterpriseEdition-${hashedKey}`],
{ revalidate: 60 * 60 * 24 }
)();
const previousResult = await getPreviousResult();
if (previousResult.active === null) {
if (isValid === null) {
await setPreviousResult({ active: false, lastChecked: new Date() });
return false;
}
}
if (isValid !== null) {
await setPreviousResult({ active: isValid, lastChecked: new Date() });
return isValid;
} else {
// if result is undefined -> error
// if the last check was less than 24 hours, return the previous value:
if (new Date().getTime() - previousResult.lastChecked.getTime() <= 24 * 60 * 60 * 1000) {
return previousResult.active !== null ? previousResult.active : false;
}
// if the last check was more than 24 hours, throw an error
throw new Error("Error while checking license");
}
};
export const getRemoveInAppBrandingPermission = (team: TTeam): boolean => {
@@ -22,20 +143,20 @@ export const getRemoveLinkBrandingPermission = (team: TTeam): boolean => {
else return false;
};
export const getRoleManagementPermission = (team: TTeam): boolean => {
export const getRoleManagementPermission = async (team: TTeam): Promise<boolean> => {
if (IS_FORMBRICKS_CLOUD) return team.billing.features.inAppSurvey.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return getIsEnterpriseEdition();
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
else return false;
};
export const getAdvancedTargetingPermission = (team: TTeam): boolean => {
export const getAdvancedTargetingPermission = async (team: TTeam): Promise<boolean> => {
if (IS_FORMBRICKS_CLOUD) return team.billing.features.userTargeting.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return getIsEnterpriseEdition();
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
else return false;
};
export const getMultiLanguagePermission = (team: TTeam): boolean => {
export const getMultiLanguagePermission = async (team: TTeam): Promise<boolean> => {
if (IS_FORMBRICKS_CLOUD) return team.billing.features.inAppSurvey.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return getIsEnterpriseEdition();
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
else return false;
};

View File

@@ -4,6 +4,8 @@
import { unstable_cache } from "next/cache";
import { parse, stringify } from "superjson";
export { revalidateTag } from "next/cache";
export const cache = <T, P extends unknown[]>(
fn: (...params: P) => Promise<T>,
keys: Parameters<typeof unstable_cache>[1],

View File

@@ -162,6 +162,7 @@ export const SYNC_USER_IDENTIFICATION_RATE_LIMIT = {
};
export const DEBUG = env.DEBUG === "1";
export const E2E_TESTING = env.E2E_TESTING === "1";
// Enterprise License constant
export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY;

View File

@@ -18,6 +18,7 @@ export const env = createEnv({
DEBUG: z.enum(["1", "0"]).optional(),
DEFAULT_TEAM_ID: z.string().optional(),
DEFAULT_TEAM_ROLE: z.enum(["owner", "admin", "editor", "developer", "viewer"]).optional(),
E2E_TESTING: z.enum(["1", "0"]).optional(),
EMAIL_AUTH_DISABLED: z.enum(["1", "0"]).optional(),
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
ENCRYPTION_KEY: z.string().length(64).or(z.string().length(32)),
@@ -121,6 +122,7 @@ export const env = createEnv({
DEBUG: process.env.DEBUG,
DEFAULT_TEAM_ID: process.env.DEFAULT_TEAM_ID,
DEFAULT_TEAM_ROLE: process.env.DEFAULT_TEAM_ROLE,
E2E_TESTING: process.env.E2E_TESTING,
EMAIL_AUTH_DISABLED: process.env.EMAIL_AUTH_DISABLED,
EMAIL_VERIFICATION_DISABLED: process.env.EMAIL_VERIFICATION_DISABLED,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,

View File

@@ -1,17 +0,0 @@
import { ENTERPRISE_LICENSE_KEY, IS_FORMBRICKS_CLOUD } from "../constants";
import { getTeam } from "../team/service";
export const getIsEnterpriseEdition = (): boolean => {
if (ENTERPRISE_LICENSE_KEY) {
return ENTERPRISE_LICENSE_KEY.length > 0;
}
return false;
};
export const getMultiLanguagePermission = async (teamId: string): Promise<boolean> => {
const team = await getTeam(teamId);
if (!team) return false;
if (IS_FORMBRICKS_CLOUD) return team.billing.features.inAppSurvey.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return getIsEnterpriseEdition();
else return false;
};

View File

@@ -67,6 +67,7 @@
"CUSTOMER_IO_API_KEY",
"CUSTOMER_IO_SITE_ID",
"DEBUG",
"E2E_TESTING",
"EMAIL_AUTH_DISABLED",
"EMAIL_VERIFICATION_DISABLED",
"ENCRYPTION_KEY",