mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-11 18:58:45 -06:00
Compare commits
4 Commits
feat/css-v
...
feat/7224-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e039add5 | ||
|
|
e6d02df599 | ||
|
|
504a059af5 | ||
|
|
9d7523542a |
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { TFunction } from "i18next";
|
||||
import { RotateCcwIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { recheckLicenseAction } from "@/modules/ee/license-check/actions";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { SettingsCard } from "../../../components/SettingsCard";
|
||||
|
||||
type LicenseStatus = "active" | "expired" | "unreachable" | "no-license";
|
||||
|
||||
interface EnterpriseLicenseStatusProps {
|
||||
status: LicenseStatus;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
const getBadgeConfig = (
|
||||
status: LicenseStatus,
|
||||
t: TFunction
|
||||
): { type: "success" | "error" | "warning" | "gray"; label: string } => {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return { type: "success", label: t("environments.settings.enterprise.license_status_active") };
|
||||
case "expired":
|
||||
return { type: "error", label: t("environments.settings.enterprise.license_status_expired") };
|
||||
case "unreachable":
|
||||
return { type: "warning", label: t("environments.settings.enterprise.license_status_unreachable") };
|
||||
case "no-license":
|
||||
return { type: "gray", label: t("environments.settings.enterprise.license_status_no_license") };
|
||||
default:
|
||||
return { type: "gray", label: t("environments.settings.enterprise.license_status") };
|
||||
}
|
||||
};
|
||||
|
||||
export const EnterpriseLicenseStatus = ({ status, environmentId }: EnterpriseLicenseStatusProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [isRechecking, setIsRechecking] = useState(false);
|
||||
|
||||
const handleRecheck = async () => {
|
||||
setIsRechecking(true);
|
||||
try {
|
||||
const result = await recheckLicenseAction({ environmentId });
|
||||
if (result?.serverError) {
|
||||
toast.error(result.serverError || t("environments.settings.enterprise.recheck_license_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result?.data) {
|
||||
if (result.data.status === "unreachable") {
|
||||
toast.error(t("environments.settings.enterprise.recheck_license_unreachable"));
|
||||
} else {
|
||||
toast.success(t("environments.settings.enterprise.recheck_license_success"));
|
||||
}
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(t("environments.settings.enterprise.recheck_license_failed"));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t("environments.settings.enterprise.recheck_license_failed")
|
||||
);
|
||||
} finally {
|
||||
setIsRechecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const badgeConfig = getBadgeConfig(status, t);
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
title={t("environments.settings.enterprise.license_status")}
|
||||
description={t("environments.settings.enterprise.license_status_description")}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Badge type={badgeConfig.type} text={badgeConfig.label} size="normal" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRecheck}
|
||||
disabled={isRechecking}
|
||||
className="shrink-0">
|
||||
{isRechecking ? (
|
||||
<>
|
||||
<RotateCcwIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t("environments.settings.enterprise.rechecking")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||
{t("environments.settings.enterprise.recheck_license")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="border-t border-slate-100 pt-4 text-sm text-slate-500">
|
||||
{t("environments.settings.enterprise.questions_please_reach_out_to")}{" "}
|
||||
<a
|
||||
className="font-medium text-slate-700 underline hover:text-slate-900"
|
||||
href="mailto:hola@formbricks.com">
|
||||
hola@formbricks.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { EnterpriseLicenseStatus } from "@/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/components/EnterpriseLicenseStatus";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
|
||||
@@ -25,7 +26,8 @@ const Page = async (props) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { active: isEnterpriseEdition } = await getEnterpriseLicense();
|
||||
const licenseState = await getEnterpriseLicense();
|
||||
const hasLicense = licenseState.status !== "no-license";
|
||||
|
||||
const paidFeatures = [
|
||||
{
|
||||
@@ -90,35 +92,17 @@ const Page = async (props) => {
|
||||
activeId="enterprise"
|
||||
/>
|
||||
</PageHeader>
|
||||
{isEnterpriseEdition ? (
|
||||
<div>
|
||||
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
|
||||
<div className="space-y-4 p-8">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
|
||||
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-400" />
|
||||
</div>
|
||||
<p className="text-slate-800">
|
||||
{t(
|
||||
"environments.settings.enterprise.your_enterprise_license_is_active_all_features_unlocked"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">
|
||||
{t("environments.settings.enterprise.questions_please_reach_out_to")}{" "}
|
||||
<a className="font-semibold underline" href="mailto:hola@formbricks.com">
|
||||
hola@formbricks.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{hasLicense ? (
|
||||
<EnterpriseLicenseStatus
|
||||
status={licenseState.status}
|
||||
environmentId={params.environmentId}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
||||
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
||||
aria-hidden="true">
|
||||
<circle
|
||||
cx={512}
|
||||
@@ -153,8 +137,8 @@ const Page = async (props) => {
|
||||
{t("environments.settings.enterprise.enterprise_features")}
|
||||
</h2>
|
||||
<ul className="my-4 space-y-4">
|
||||
{paidFeatures.map((feature, index) => (
|
||||
<li key={index} className="flex items-center">
|
||||
{paidFeatures.map((feature) => (
|
||||
<li key={feature.title} className="flex items-center">
|
||||
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
|
||||
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-400" />
|
||||
</div>
|
||||
|
||||
@@ -897,11 +897,22 @@ checksums:
|
||||
environments/settings/enterprise/enterprise_features: 3271476140733924b2a2477c4fdf3d12
|
||||
environments/settings/enterprise/get_an_enterprise_license_to_get_access_to_all_features: afd3c00f19097e88ed051800979eea44
|
||||
environments/settings/enterprise/keep_full_control_over_your_data_privacy_and_security: 43aa041cc3e2b2fdd35d2d34659a6b7a
|
||||
environments/settings/enterprise/license_status: f6f85c59074ca2455321bd5288d94be8
|
||||
environments/settings/enterprise/license_status_active: 3e1ec025c4a50830bbb9ad57a176630a
|
||||
environments/settings/enterprise/license_status_description: 828e4527f606471cd8cf58b55ff824f6
|
||||
environments/settings/enterprise/license_status_expired: 63b27cccba4ab2143e0f5f3d46e4168a
|
||||
environments/settings/enterprise/license_status_no_license: dbffdac22aeaff8ef85335dc47ceefc3
|
||||
environments/settings/enterprise/license_status_unreachable: 202b110dab099f1167b13c326349e570
|
||||
environments/settings/enterprise/no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form: daef55124d4363526008eb91a0b68246
|
||||
environments/settings/enterprise/no_credit_card_no_sales_call_just_test_it: 18f9859cdf12537b7019ecdb0a0a2b53
|
||||
environments/settings/enterprise/on_request: cf9949748c15313a8fd57bf965bec16b
|
||||
environments/settings/enterprise/organization_roles: 731d5028521c2a3a7bdbd7ed215dd861
|
||||
environments/settings/enterprise/questions_please_reach_out_to: ac4be65ffef9349eaeb137c254d3fee7
|
||||
environments/settings/enterprise/recheck_license: b913b64f89df184b5059710f4a0b26fa
|
||||
environments/settings/enterprise/recheck_license_failed: dd410acbb8887625cf194189f832dd7c
|
||||
environments/settings/enterprise/recheck_license_success: 700ddd805be904a415f614de3df1da78
|
||||
environments/settings/enterprise/recheck_license_unreachable: 0ca81bd89595a9da24bc94dcef132175
|
||||
environments/settings/enterprise/rechecking: 54c454aa8e4d27363543349b7c2a28bc
|
||||
environments/settings/enterprise/request_30_day_trial_license: 8d5a1b5d9f0790783693122ac30c16ef
|
||||
environments/settings/enterprise/saml_sso: 86b76024524fc585b2c3950126ef6f62
|
||||
environments/settings/enterprise/service_level_agreement: e31e74f66f5c7c79e82878f4f68abc37
|
||||
@@ -909,7 +920,6 @@ checksums:
|
||||
environments/settings/enterprise/sso: 95e98e279bb89233d63549b202bd9112
|
||||
environments/settings/enterprise/teams: 21ab78abcba0f16c3029741563f789ea
|
||||
environments/settings/enterprise/unlock_the_full_power_of_formbricks_free_for_30_days: 104d07b63a42911c9673ceb08a4dbd43
|
||||
environments/settings/enterprise/your_enterprise_license_is_active_all_features_unlocked: f03f3c7a81f61eb5cd78fa7ad32896f8
|
||||
environments/settings/general/bulk_invite_warning_description: e8737a2fbd5ff353db5580d17b4b5a37
|
||||
environments/settings/general/cannot_delete_only_organization: 833cc6848b28f2694a4552b4de91a6ba
|
||||
environments/settings/general/cannot_leave_only_organization: dd8463262e4299fef7ad73512225c55b
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Unternehmensfunktionen",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Hol dir eine Enterprise-Lizenz, um Zugriff auf alle Funktionen zu erhalten.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Behalte die volle Kontrolle über deine Daten, Privatsphäre und Sicherheit.",
|
||||
"license_status": "Lizenzstatus",
|
||||
"license_status_active": "Aktiv",
|
||||
"license_status_description": "Status deiner Enterprise-Lizenz.",
|
||||
"license_status_expired": "Abgelaufen",
|
||||
"license_status_no_license": "Keine Lizenz",
|
||||
"license_status_unreachable": "Nicht erreichbar",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Ganz unkompliziert: Fordere eine kostenlose 30-Tage-Testlizenz an, um alle Funktionen zu testen, indem Du dieses Formular ausfüllst:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Keine Kreditkarte. Kein Verkaufsgespräch. Einfach testen :)",
|
||||
"on_request": "Auf Anfrage",
|
||||
"organization_roles": "Organisationsrollen (Admin, Editor, Entwickler, etc.)",
|
||||
"questions_please_reach_out_to": "Fragen? Bitte melde Dich bei",
|
||||
"recheck_license": "Lizenz erneut prüfen",
|
||||
"recheck_license_failed": "Lizenzprüfung fehlgeschlagen. Der Lizenzserver ist möglicherweise nicht erreichbar.",
|
||||
"recheck_license_success": "Lizenzprüfung erfolgreich",
|
||||
"recheck_license_unreachable": "Lizenzserver ist nicht erreichbar. Bitte versuche es später erneut.",
|
||||
"rechecking": "Wird erneut geprüft...",
|
||||
"request_30_day_trial_license": "30-Tage-Testlizenz anfordern",
|
||||
"saml_sso": "SAML-SSO",
|
||||
"service_level_agreement": "Service-Level-Vereinbarung",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2-, HIPAA- und ISO 27001-Konformitätsprüfung",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Teams & Zugriffskontrolle (Lesen, Lesen & Schreiben, Verwalten)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Schalte die volle Power von Formbricks frei. 30 Tage kostenlos.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Deine Unternehmenslizenz ist aktiv. Alle Funktionen freigeschaltet."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Schalte die volle Power von Formbricks frei. 30 Tage kostenlos."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "Bitte beachte, dass im Free-Plan alle Organisationsmitglieder automatisch die Rolle \"Owner\" zugewiesen bekommen, unabhängig von der im CSV-File angegebenen Rolle.",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Enterprise Features",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Get an Enterprise license to get access to all features.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Keep full control over your data privacy and security.",
|
||||
"license_status": "License Status",
|
||||
"license_status_active": "Active",
|
||||
"license_status_description": "Status of your enterprise license.",
|
||||
"license_status_expired": "Expired",
|
||||
"license_status_no_license": "No License",
|
||||
"license_status_unreachable": "Unreachable",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "No call needed, no strings attached: Request a free 30-day trial license to test all features by filling out this form:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "No credit card. No sales call. Just test it :)",
|
||||
"on_request": "On request",
|
||||
"organization_roles": "Organization Roles (Admin, Editor, Developer, etc.)",
|
||||
"questions_please_reach_out_to": "Questions? Please reach out to",
|
||||
"recheck_license": "Recheck license",
|
||||
"recheck_license_failed": "License check failed. The license server may be unreachable.",
|
||||
"recheck_license_success": "License check successful",
|
||||
"recheck_license_unreachable": "License server is unreachable. Please try again later.",
|
||||
"rechecking": "Rechecking...",
|
||||
"request_30_day_trial_license": "Request 30-day Trial License",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "Service Level Agreement",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 Compliance check",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Teams & Access Roles (Read, Read & Write, Manage)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Unlock the full power of Formbricks. Free for 30 days.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Your Enterprise License is active. All features unlocked."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Unlock the full power of Formbricks. Free for 30 days."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "On the free plan, all organization members are always assigned the “Owner” role.",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Características empresariales",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obtén una licencia empresarial para acceder a todas las características.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Mantén el control total sobre la privacidad y seguridad de tus datos.",
|
||||
"license_status": "Estado de la licencia",
|
||||
"license_status_active": "Activa",
|
||||
"license_status_description": "Estado de tu licencia enterprise.",
|
||||
"license_status_expired": "Caducada",
|
||||
"license_status_no_license": "Sin licencia",
|
||||
"license_status_unreachable": "Inaccesible",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sin necesidad de llamadas, sin compromisos: solicita una licencia de prueba gratuita de 30 días para probar todas las características rellenando este formulario:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Sin tarjeta de crédito. Sin llamada de ventas. Solo pruébalo :)",
|
||||
"on_request": "Bajo petición",
|
||||
"organization_roles": "Roles de organización (administrador, editor, desarrollador, etc.)",
|
||||
"questions_please_reach_out_to": "¿Preguntas? Por favor, contacta con",
|
||||
"recheck_license": "Volver a comprobar licencia",
|
||||
"recheck_license_failed": "Error al comprobar la licencia. Es posible que el servidor de licencias no esté disponible.",
|
||||
"recheck_license_success": "Comprobación de licencia correcta",
|
||||
"recheck_license_unreachable": "El servidor de licencias no está disponible. Inténtalo de nuevo más tarde.",
|
||||
"rechecking": "Comprobando...",
|
||||
"request_30_day_trial_license": "Solicitar licencia de prueba de 30 días",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "Acuerdo de nivel de servicio",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "Verificación de cumplimiento SOC2, HIPAA, ISO 27001",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Equipos y roles de acceso (lectura, lectura y escritura, gestión)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloquea todo el potencial de Formbricks. Gratis durante 30 días.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Tu licencia empresarial está activa. Todas las características desbloqueadas."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloquea todo el potencial de Formbricks. Gratis durante 30 días."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "En el plan gratuito, a todos los miembros de la organización se les asigna siempre el rol de \"Propietario\".",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Fonctionnalités d'entreprise",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenez une licence Entreprise pour accéder à toutes les fonctionnalités.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Gardez un contrôle total sur la confidentialité et la sécurité de vos données.",
|
||||
"license_status": "Statut de la licence",
|
||||
"license_status_active": "Active",
|
||||
"license_status_description": "Statut de votre licence entreprise.",
|
||||
"license_status_expired": "Expirée",
|
||||
"license_status_no_license": "Aucune licence",
|
||||
"license_status_unreachable": "Inaccessible",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Aucun appel nécessaire, aucune obligation : Demandez une licence d'essai gratuite de 30 jours pour tester toutes les fonctionnalités en remplissant ce formulaire :",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Aucune carte de crédit. Aucun appel de vente. Testez-le simplement :)",
|
||||
"on_request": "Sur demande",
|
||||
"organization_roles": "Rôles d'organisation (Administrateur, Éditeur, Développeur, etc.)",
|
||||
"questions_please_reach_out_to": "Des questions ? Veuillez contacter",
|
||||
"recheck_license": "Revérifier la licence",
|
||||
"recheck_license_failed": "La vérification de la licence a échoué. Le serveur de licences est peut-être inaccessible.",
|
||||
"recheck_license_success": "Vérification de la licence réussie",
|
||||
"recheck_license_unreachable": "Le serveur de licences est inaccessible. Veuillez réessayer plus tard.",
|
||||
"rechecking": "Revérification en cours...",
|
||||
"request_30_day_trial_license": "Demander une licence d'essai de 30 jours",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "Accord de niveau de service",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "Vérification de conformité SOC2, HIPAA, ISO 27001",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Équipes et Rôles d'Accès (Lire, Lire et Écrire, Gérer)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Débloquez tout le potentiel de Formbricks. Gratuit pendant 30 jours.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Votre licence d'entreprise est active. Toutes les fonctionnalités sont déverrouillées."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Débloquez tout le potentiel de Formbricks. Gratuit pendant 30 jours."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "Dans le plan gratuit, tous les membres de l'organisation se voient toujours attribuer le rôle \"Owner\".",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Vállalati funkciók",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Vállalati licenc megszerzése az összes funkcióhoz való hozzáféréshez.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Az adatvédelem és biztonság fölötti rendelkezés teljes kézben tartása.",
|
||||
"license_status": "Licenc állapota",
|
||||
"license_status_active": "Aktív",
|
||||
"license_status_description": "A vállalati licenced állapota.",
|
||||
"license_status_expired": "Lejárt",
|
||||
"license_status_no_license": "Nincs licenc",
|
||||
"license_status_unreachable": "Nem elérhető",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Nincs szükség telefonálásra, nincs feltételekhez kötöttség: kérjen 30 napos ingyenes próbalicencet az összes funkció kipróbálásához az alábbi űrlap kitöltésével:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Nem kell hitelkártya. Nincsenek értékesítési hívások. Egyszerűen csak próbálja ki :)",
|
||||
"on_request": "Kérésre",
|
||||
"organization_roles": "Szervezeti szerepek (adminisztrátor, szerkesztő, fejlesztő stb.)",
|
||||
"questions_please_reach_out_to": "Kérdése van? Írjon nekünk erre az e-mail-címre:",
|
||||
"recheck_license": "Licenc újraellenőrzése",
|
||||
"recheck_license_failed": "A licenc ellenőrzése sikertelen. Előfordulhat, hogy a licenckiszolgáló nem elérhető.",
|
||||
"recheck_license_success": "A licenc ellenőrzése sikeres",
|
||||
"recheck_license_unreachable": "A licenckiszolgáló nem elérhető. Kérjük, próbáld újra később.",
|
||||
"rechecking": "Újraellenőrzés...",
|
||||
"request_30_day_trial_license": "30 napos ingyenes licenc kérése",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "Szolgáltatási megállapodás",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 megfelelőségi ellenőrzés",
|
||||
"sso": "SSO (Google, Microsoft, OpenID-kapcsolat)",
|
||||
"teams": "Csapatok és hozzáférési szerepek (olvasás, olvasás és írás, kezelés)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "A Formbricks teljes erejének feloldása. 30 napig ingyen.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "A vállalati licence aktív. Az összes funkció feloldva."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "A Formbricks teljes erejének feloldása. 30 napig ingyen."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "Az ingyenes csomagban az összes szervezeti tag mindig a „Tulajdonos” szerephez van hozzárendelve.",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "エンタープライズ機能",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "すべての機能にアクセスするには、エンタープライズライセンスを取得してください。",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "データのプライバシーとセキュリティを完全に制御できます。",
|
||||
"license_status": "ライセンスステータス",
|
||||
"license_status_active": "有効",
|
||||
"license_status_description": "エンタープライズライセンスのステータス。",
|
||||
"license_status_expired": "期限切れ",
|
||||
"license_status_no_license": "ライセンスなし",
|
||||
"license_status_unreachable": "接続不可",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "電話不要、制約なし: このフォームに記入して、すべての機能をテストするための無料の30日間トライアルライセンスをリクエストしてください:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "クレジットカード不要。営業電話もありません。ただテストしてください :)",
|
||||
"on_request": "リクエストに応じて",
|
||||
"organization_roles": "組織ロール(管理者、編集者、開発者など)",
|
||||
"questions_please_reach_out_to": "質問はありますか?こちらまでお問い合わせください",
|
||||
"recheck_license": "ライセンスを再確認",
|
||||
"recheck_license_failed": "ライセンスの確認に失敗しました。ライセンスサーバーに接続できない可能性があります。",
|
||||
"recheck_license_success": "ライセンスの確認に成功しました",
|
||||
"recheck_license_unreachable": "ライセンスサーバーに接続できません。後ほど再度お試しください。",
|
||||
"rechecking": "再確認中...",
|
||||
"request_30_day_trial_license": "30日間トライアルライセンスをリクエスト",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "サービスレベル契約",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2、HIPAA、ISO 27001準拠チェック",
|
||||
"sso": "SSO(Google、Microsoft、OpenID Connect)",
|
||||
"teams": "チーム&アクセスロール(読み取り、読み書き、管理)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Formbricksの全機能をアンロック。30日間無料。",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "あなたのエンタープライズライセンスは有効です。すべての機能がアンロックされました。"
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Formbricksの全機能をアンロック。30日間無料。"
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "無料プランでは、すべての組織メンバーに常に「オーナー」ロールが割り当てられます。",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Enterprise-functies",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Ontvang een Enterprise-licentie om toegang te krijgen tot alle functies.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Houd de volledige controle over de privacy en beveiliging van uw gegevens.",
|
||||
"license_status": "Licentiestatus",
|
||||
"license_status_active": "Actief",
|
||||
"license_status_description": "Status van je enterprise-licentie.",
|
||||
"license_status_expired": "Verlopen",
|
||||
"license_status_no_license": "Geen licentie",
|
||||
"license_status_unreachable": "Niet bereikbaar",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Geen telefoontje nodig, geen verplichtingen: vraag een gratis proeflicentie van 30 dagen aan om alle functies te testen door dit formulier in te vullen:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Geen creditcard. Geen verkoopgesprek. Gewoon testen :)",
|
||||
"on_request": "Op aanvraag",
|
||||
"organization_roles": "Organisatierollen (beheerder, redacteur, ontwikkelaar, etc.)",
|
||||
"questions_please_reach_out_to": "Vragen? Neem contact op met",
|
||||
"recheck_license": "Licentie opnieuw controleren",
|
||||
"recheck_license_failed": "Licentiecontrole mislukt. De licentieserver is mogelijk niet bereikbaar.",
|
||||
"recheck_license_success": "Licentiecontrole geslaagd",
|
||||
"recheck_license_unreachable": "Licentieserver is niet bereikbaar. Probeer het later opnieuw.",
|
||||
"rechecking": "Opnieuw controleren...",
|
||||
"request_30_day_trial_license": "Vraag een proeflicentie van 30 dagen aan",
|
||||
"saml_sso": "SAML-SSO",
|
||||
"service_level_agreement": "Service Level Overeenkomst",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 Conformiteitscontrole",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Teams en toegangsrollen (lezen, lezen en schrijven, beheren)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Ontgrendel de volledige kracht van Formbricks. 30 dagen gratis.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Uw Enterprise-licentie is actief. Alle functies ontgrendeld."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Ontgrendel de volledige kracht van Formbricks. 30 dagen gratis."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "Bij het gratis abonnement krijgen alle organisatieleden altijd de rol 'Eigenaar' toegewezen.",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Recursos Empresariais",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Adquira uma licença Enterprise para ter acesso a todos os recursos.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controle total sobre a privacidade e segurança dos seus dados.",
|
||||
"license_status": "Status da licença",
|
||||
"license_status_active": "Ativa",
|
||||
"license_status_description": "Status da sua licença enterprise.",
|
||||
"license_status_expired": "Expirada",
|
||||
"license_status_no_license": "Sem licença",
|
||||
"license_status_unreachable": "Inacessível",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sem necessidade de ligação, sem compromisso: Solicite uma licença de teste gratuita de 30 dias para testar todas as funcionalidades preenchendo este formulário:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Sem cartão de crédito. Sem ligação de vendas. Só teste :)",
|
||||
"on_request": "Quando solicitado",
|
||||
"organization_roles": "Funções na Organização (Admin, Editor, Desenvolvedor, etc.)",
|
||||
"questions_please_reach_out_to": "Perguntas? Entre em contato com",
|
||||
"recheck_license": "Verificar licença novamente",
|
||||
"recheck_license_failed": "Falha na verificação da licença. O servidor de licenças pode estar inacessível.",
|
||||
"recheck_license_success": "Verificação da licença bem-sucedida",
|
||||
"recheck_license_unreachable": "Servidor de licenças inacessível. Por favor, tente novamente mais tarde.",
|
||||
"rechecking": "Verificando novamente...",
|
||||
"request_30_day_trial_license": "Pedir Licença de Teste de 30 Dias",
|
||||
"saml_sso": "SSO SAML",
|
||||
"service_level_agreement": "Acordo de Nível de Serviço",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "Verificação de conformidade SOC2, HIPAA, ISO 27001",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Equipes e Funções de Acesso (Ler, Ler e Escrever, Gerenciar)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Sua licença empresarial está ativa. Todos os recursos estão desbloqueados."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "Por favor, note que no Plano Gratuito, todos os membros da organização são automaticamente atribuídos ao papel de 'Owner', independentemente do papel especificado no arquivo CSV.",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Funcionalidades da Empresa",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obtenha uma licença Enterprise para ter acesso a todas as funcionalidades.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Mantenha controlo total sobre a privacidade e segurança dos seus dados.",
|
||||
"license_status": "Estado da licença",
|
||||
"license_status_active": "Ativa",
|
||||
"license_status_description": "Estado da sua licença empresarial.",
|
||||
"license_status_expired": "Expirada",
|
||||
"license_status_no_license": "Sem licença",
|
||||
"license_status_unreachable": "Inacessível",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Sem necessidade de chamada, sem compromissos: Solicite uma licença de teste gratuita de 30 dias para testar todas as funcionalidades preenchendo este formulário:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Sem cartão de crédito. Sem chamada de vendas. Apenas teste :)",
|
||||
"on_request": "A pedido",
|
||||
"organization_roles": "Funções da Organização (Administrador, Editor, Programador, etc.)",
|
||||
"questions_please_reach_out_to": "Questões? Por favor entre em contacto com",
|
||||
"recheck_license": "Verificar licença novamente",
|
||||
"recheck_license_failed": "A verificação da licença falhou. O servidor de licenças pode estar inacessível.",
|
||||
"recheck_license_success": "Verificação da licença bem-sucedida",
|
||||
"recheck_license_unreachable": "O servidor de licenças está inacessível. Por favor, tenta novamente mais tarde.",
|
||||
"rechecking": "A verificar novamente...",
|
||||
"request_30_day_trial_license": "Solicitar Licença de Teste de 30 Dias",
|
||||
"saml_sso": "SSO SAML",
|
||||
"service_level_agreement": "Acordo de Nível de Serviço",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "Verificação de conformidade SOC2, HIPAA, ISO 27001",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Equipas e Funções de Acesso (Ler, Ler e Escrever, Gerir)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "A sua licença Enterprise está ativa. Todas as funcionalidades desbloqueadas."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "No plano gratuito, todos os membros da organização são sempre atribuídos ao papel de \"Proprietário\".",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Funcții Enterprise",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Obțineți o licență Enterprise pentru a avea acces la toate funcționalitățile.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Mențineți controlul complet asupra confidențialității și securității datelor dumneavoastră.",
|
||||
"license_status": "Stare licență",
|
||||
"license_status_active": "Activă",
|
||||
"license_status_description": "Starea licenței tale enterprise.",
|
||||
"license_status_expired": "Expirată",
|
||||
"license_status_no_license": "Fără licență",
|
||||
"license_status_unreachable": "Indisponibilă",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Nicio apel necesar, fără obligații: Solicitați o licență de probă gratuită de 30 de zile pentru a testa toate funcțiile prin completarea acestui formular:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Nu este nevoie de card de credit. Fără apeluri de vânzări. Doar testează-l :)",
|
||||
"on_request": "La cerere",
|
||||
"organization_roles": "Roluri organizaționale (Administrator, Editor, Dezvoltator, etc.)",
|
||||
"questions_please_reach_out_to": "Întrebări? Vă rugăm să trimiteți mesaj către",
|
||||
"recheck_license": "Verifică din nou licența",
|
||||
"recheck_license_failed": "Verificarea licenței a eșuat. Serverul de licențe poate fi indisponibil.",
|
||||
"recheck_license_success": "Licența a fost verificată cu succes",
|
||||
"recheck_license_unreachable": "Serverul de licențe este indisponibil. Te rugăm să încerci din nou mai târziu.",
|
||||
"rechecking": "Se verifică din nou...",
|
||||
"request_30_day_trial_license": "Solicitați o licență de încercare de 30 de zile",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "Acord privind nivelul de servicii",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "Verificare conformitate SOC2, HIPAA, ISO 27001",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Echipe & Roluri de Acces (Citiți, Citiți și Scrieți, Gestionați)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Deblocați puterea completă a Formbricks. Gratuit timp de 30 de zile.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Licența dvs. Enterprise este activă. Toate funcțiile sunt deblocate."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Deblocați puterea completă a Formbricks. Gratuit timp de 30 de zile."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "În planul gratuit, toți membrii organizației sunt întotdeauna alocați rolului „Proprietar”.",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Функции для предприятий",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Получите корпоративную лицензию для доступа ко всем функциям.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Полный контроль над конфиденциальностью и безопасностью ваших данных.",
|
||||
"license_status": "Статус лицензии",
|
||||
"license_status_active": "Активна",
|
||||
"license_status_description": "Статус вашей корпоративной лицензии.",
|
||||
"license_status_expired": "Срок действия истёк",
|
||||
"license_status_no_license": "Нет лицензии",
|
||||
"license_status_unreachable": "Недоступна",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Без звонков и обязательств: запросите бесплатную 30-дневную пробную лицензию для тестирования всех функций, заполнив эту форму:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Без кредитной карты. Без звонков от отдела продаж. Просто попробуйте :)",
|
||||
"on_request": "По запросу",
|
||||
"organization_roles": "Роли в организации (администратор, редактор, разработчик и др.)",
|
||||
"questions_please_reach_out_to": "Вопросы? Свяжитесь с",
|
||||
"recheck_license": "Проверить лицензию ещё раз",
|
||||
"recheck_license_failed": "Не удалось проверить лицензию. Сервер лицензий может быть недоступен.",
|
||||
"recheck_license_success": "Проверка лицензии прошла успешно",
|
||||
"recheck_license_unreachable": "Сервер лицензий недоступен. Пожалуйста, попробуй позже.",
|
||||
"rechecking": "Проверка...",
|
||||
"request_30_day_trial_license": "Запросить 30-дневную пробную лицензию",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "Соглашение об уровне обслуживания (SLA)",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "Проверка соответствия SOC2, HIPAA, ISO 27001",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Команды и роли доступа (чтение, чтение и запись, управление)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Откройте все возможности Formbricks. Бесплатно на 30 дней.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Ваша корпоративная лицензия активна. Все функции разблокированы."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Откройте все возможности Formbricks. Бесплатно на 30 дней."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "В бесплатном тарифе всем участникам организации всегда назначается роль \"Владелец\".",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "Enterprise-funktioner",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "Skaffa en Enterprise-licens för att få tillgång till alla funktioner.",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "Behåll full kontroll över din datasekretess och säkerhet.",
|
||||
"license_status": "Licensstatus",
|
||||
"license_status_active": "Aktiv",
|
||||
"license_status_description": "Status för din företagslicens.",
|
||||
"license_status_expired": "Utgången",
|
||||
"license_status_no_license": "Ingen licens",
|
||||
"license_status_unreachable": "Otillgänglig",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "Inget samtal behövs, inga åtaganden: Begär en gratis 30-dagars provlicens för att testa alla funktioner genom att fylla i detta formulär:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "Inget kreditkort. Inget säljsamtal. Testa bara :)",
|
||||
"on_request": "På begäran",
|
||||
"organization_roles": "Organisationsroller (Admin, Redaktör, Utvecklare, etc.)",
|
||||
"questions_please_reach_out_to": "Frågor? Kontakta",
|
||||
"recheck_license": "Kontrollera licensen igen",
|
||||
"recheck_license_failed": "Licenskontrollen misslyckades. Licensservern kan vara otillgänglig.",
|
||||
"recheck_license_success": "Licenskontrollen lyckades",
|
||||
"recheck_license_unreachable": "Licensservern är otillgänglig. Försök igen senare.",
|
||||
"rechecking": "Kontrollerar igen...",
|
||||
"request_30_day_trial_license": "Begär 30-dagars provlicens",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "Servicenivåavtal",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2, HIPAA, ISO 27001 efterlevnadskontroll",
|
||||
"sso": "SSO (Google, Microsoft, OpenID Connect)",
|
||||
"teams": "Team och åtkomstroller (Läs, Läs och skriv, Hantera)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Lås upp Formbricks fulla kraft. Gratis i 30 dagar.",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "Din Enterprise-licens är aktiv. Alla funktioner upplåsta."
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "Lås upp Formbricks fulla kraft. Gratis i 30 dagar."
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "På gratisplanen tilldelas alla organisationsmedlemmar alltid rollen \"Ägare\".",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "企业 功能",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "获取 企业 许可证 来 访问 所有 功能。",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "保持 对 您 的 数据 隐私 和 安全 的 完全 控制。",
|
||||
"license_status": "许可证状态",
|
||||
"license_status_active": "已激活",
|
||||
"license_status_description": "你的企业许可证状态。",
|
||||
"license_status_expired": "已过期",
|
||||
"license_status_no_license": "无许可证",
|
||||
"license_status_unreachable": "无法访问",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "无需 电话 ,无需 附加 条件: 申请 免费 30 天 试用 授权以 通过 填写 此 表格 测试 所有 功能:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "无需信用卡 。无需销售电话 。只需测试一下 :)",
|
||||
"on_request": "按请求",
|
||||
"organization_roles": "组织角色(管理员,编辑,开发者等)",
|
||||
"questions_please_reach_out_to": "问题 ? 请 联系",
|
||||
"recheck_license": "重新检查许可证",
|
||||
"recheck_license_failed": "许可证检查失败。许可证服务器可能无法访问。",
|
||||
"recheck_license_success": "许可证检查成功",
|
||||
"recheck_license_unreachable": "许可证服务器无法访问,请稍后再试。",
|
||||
"rechecking": "正在重新检查...",
|
||||
"request_30_day_trial_license": "申请 30 天 的 试用许可证",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "服务水平协议",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2 , HIPAA , ISO 27001 合规检查",
|
||||
"sso": "SSO (Google 、Microsoft 、OpenID Connect)",
|
||||
"teams": "团队 & 访问 角色(读取, 读取 & 写入, 管理)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "解锁 Formbricks 的全部功能。免费使用 30 天。",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "您的企业许可证已激活 所有功能已解锁"
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "解锁 Formbricks 的全部功能。免费使用 30 天。"
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "在免费计划中,所有组织成员都会被分配为 \"Owner \"角色。",
|
||||
|
||||
@@ -956,19 +956,29 @@
|
||||
"enterprise_features": "企業版功能",
|
||||
"get_an_enterprise_license_to_get_access_to_all_features": "取得企業授權以存取所有功能。",
|
||||
"keep_full_control_over_your_data_privacy_and_security": "完全掌控您的資料隱私權和安全性。",
|
||||
"license_status": "授權狀態",
|
||||
"license_status_active": "有效",
|
||||
"license_status_description": "你的企業授權狀態。",
|
||||
"license_status_expired": "已過期",
|
||||
"license_status_no_license": "無授權",
|
||||
"license_status_unreachable": "無法連線",
|
||||
"no_call_needed_no_strings_attached_request_a_free_30_day_trial_license_to_test_all_features_by_filling_out_this_form": "無需通話,無附加條件:填寫此表單,請求免費 30 天試用授權以測試所有功能:",
|
||||
"no_credit_card_no_sales_call_just_test_it": "無需信用卡。無需銷售電話。只需測試一下 :)",
|
||||
"on_request": "依要求",
|
||||
"organization_roles": "組織角色(管理員、編輯者、開發人員等)",
|
||||
"questions_please_reach_out_to": "有任何問題?請聯絡",
|
||||
"recheck_license": "重新檢查授權",
|
||||
"recheck_license_failed": "授權檢查失敗。授權伺服器可能無法連線。",
|
||||
"recheck_license_success": "授權檢查成功",
|
||||
"recheck_license_unreachable": "授權伺服器無法連線,請稍後再試。",
|
||||
"rechecking": "正在重新檢查...",
|
||||
"request_30_day_trial_license": "請求 30 天試用授權",
|
||||
"saml_sso": "SAML SSO",
|
||||
"service_level_agreement": "服務等級協定",
|
||||
"soc2_hipaa_iso_27001_compliance_check": "SOC2、HIPAA、ISO 27001 合規性檢查",
|
||||
"sso": "SSO(Google、Microsoft、OpenID Connect)",
|
||||
"teams": "團隊和存取角色(讀取、讀取和寫入、管理)",
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "免費解鎖 Formbricks 的全部功能,為期 30 天。",
|
||||
"your_enterprise_license_is_active_all_features_unlocked": "您的企業授權處於活動狀態。所有功能都已解鎖。"
|
||||
"unlock_the_full_power_of_formbricks_free_for_30_days": "免費解鎖 Formbricks 的全部功能,為期 30 天。"
|
||||
},
|
||||
"general": {
|
||||
"bulk_invite_warning_description": "在免費方案中,所有組織成員始終會被指派「擁有者」角色。",
|
||||
|
||||
@@ -73,7 +73,12 @@ describe("rateLimitConfigs", () => {
|
||||
|
||||
test("should have all action configurations", () => {
|
||||
const actionConfigs = Object.keys(rateLimitConfigs.actions);
|
||||
expect(actionConfigs).toEqual(["emailUpdate", "surveyFollowUp", "sendLinkSurveyEmail"]);
|
||||
expect(actionConfigs).toEqual([
|
||||
"emailUpdate",
|
||||
"surveyFollowUp",
|
||||
"sendLinkSurveyEmail",
|
||||
"licenseRecheck",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export const rateLimitConfigs = {
|
||||
allowedPerInterval: 10,
|
||||
namespace: "action:send-link-survey-email",
|
||||
}, // 10 per hour
|
||||
licenseRecheck: { interval: 60, allowedPerInterval: 5, namespace: "action:license-recheck" }, // 5 per minute
|
||||
},
|
||||
|
||||
storage: {
|
||||
|
||||
91
apps/web/modules/ee/license-check/actions.ts
Normal file
91
apps/web/modules/ee/license-check/actions.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthenticationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { cache } from "@/lib/cache";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import {
|
||||
FAILED_FETCH_TTL_MS,
|
||||
FETCH_LICENSE_TTL_MS,
|
||||
clearLicenseCache,
|
||||
fetchLicenseFresh,
|
||||
getCacheKeys,
|
||||
getEnterpriseLicense,
|
||||
} from "./lib/license";
|
||||
|
||||
const ZRecheckLicenseAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
export type TRecheckLicenseAction = z.infer<typeof ZRecheckLicenseAction>;
|
||||
|
||||
export const recheckLicenseAction = authenticatedActionClient
|
||||
.schema(ZRecheckLicenseAction)
|
||||
.action(
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: TRecheckLicenseAction;
|
||||
}) => {
|
||||
// Rate limit: 5 rechecks per minute per user
|
||||
await applyRateLimit(rateLimitConfigs.actions.licenseRecheck, ctx.user.id);
|
||||
|
||||
// Only allow on self-hosted instances
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
throw new OperationNotAllowedError("License recheck is only available on self-hosted instances");
|
||||
}
|
||||
|
||||
// Get organization from environment
|
||||
const organization = await getOrganizationByEnvironmentId(parsedInput.environmentId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
// Check user is owner or manager (not member)
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(ctx.user.id, organization.id);
|
||||
if (!currentUserMembership) {
|
||||
throw new AuthenticationError("User not a member of this organization");
|
||||
}
|
||||
|
||||
if (currentUserMembership.role === "member") {
|
||||
throw new OperationNotAllowedError("Only owners and managers can recheck license");
|
||||
}
|
||||
|
||||
// Clear main license cache (preserves previous result cache for grace period)
|
||||
// This prevents instant downgrade if the license server is temporarily unreachable
|
||||
await clearLicenseCache();
|
||||
|
||||
// Fetch fresh license directly (bypasses cache)
|
||||
const freshLicense = await fetchLicenseFresh();
|
||||
|
||||
// Cache the fresh result (or null if failed) so getEnterpriseLicense can use it
|
||||
const cacheKeys = getCacheKeys();
|
||||
|
||||
if (freshLicense) {
|
||||
// Success - cache with full TTL
|
||||
await cache.set(cacheKeys.FETCH_LICENSE_CACHE_KEY, freshLicense, FETCH_LICENSE_TTL_MS);
|
||||
} else {
|
||||
// Failure - cache null with short TTL
|
||||
// The previous result cache is preserved, so grace period will still work
|
||||
await cache.set(cacheKeys.FETCH_LICENSE_CACHE_KEY, null, FAILED_FETCH_TTL_MS);
|
||||
}
|
||||
|
||||
// Now get the license state - it should use the fresh data we just cached
|
||||
// If fetch failed, it will fall back to the preserved previous result (grace period)
|
||||
const licenseState = await getEnterpriseLicense();
|
||||
|
||||
return {
|
||||
active: licenseState.active,
|
||||
status: licenseState.status,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -98,6 +98,7 @@ describe("License Core Logic", () => {
|
||||
mockCache.get.mockReset();
|
||||
mockCache.set.mockReset();
|
||||
mockCache.del.mockReset();
|
||||
mockCache.exists.mockReset();
|
||||
mockCache.withCache.mockReset();
|
||||
mockLogger.error.mockReset();
|
||||
mockLogger.warn.mockReset();
|
||||
@@ -105,9 +106,10 @@ describe("License Core Logic", () => {
|
||||
mockLogger.debug.mockReset();
|
||||
|
||||
// Set up default mock implementations for Result types
|
||||
// fetchLicense uses get + exists; getPreviousResult uses get with :previous_result key
|
||||
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: false }); // default: cache miss
|
||||
mockCache.set.mockResolvedValue({ ok: true });
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
|
||||
vi.mocked(prisma.response.count).mockResolvedValue(100);
|
||||
vi.mocked(prisma.organization.findFirst).mockResolvedValue({
|
||||
@@ -164,16 +166,22 @@ describe("License Core Logic", () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Mock cache.withCache to return cached license details (simulating cache hit)
|
||||
mockCache.withCache.mockResolvedValue(mockFetchedLicenseDetails);
|
||||
// Mock cache hit: get returns license for status key, exists returns true
|
||||
mockCache.get.mockImplementation(async (key: string) => {
|
||||
if (key.includes(":previous_result")) {
|
||||
return { ok: true, data: null };
|
||||
}
|
||||
if (key.includes(":status")) {
|
||||
return { ok: true, data: mockFetchedLicenseDetails };
|
||||
}
|
||||
return { ok: true, data: null };
|
||||
});
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
expect(license).toEqual(expectedActiveLicenseState);
|
||||
expect(mockCache.withCache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.stringContaining("fb:license:"),
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
||||
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -181,9 +189,7 @@ describe("License Core Logic", () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Mock cache.withCache to execute the function (simulating cache miss)
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
|
||||
// Default mocks give cache miss (get null, exists false)
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: mockFetchedLicenseDetails }),
|
||||
@@ -192,11 +198,8 @@ describe("License Core Logic", () => {
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(mockCache.withCache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.stringContaining("fb:license:"),
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
||||
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining("fb:license:"));
|
||||
expect(license).toEqual(expectedActiveLicenseState);
|
||||
});
|
||||
|
||||
@@ -212,11 +215,9 @@ describe("License Core Logic", () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
// Mock cache.withCache to return null (simulating fetch failure)
|
||||
mockCache.withCache.mockResolvedValue(null);
|
||||
|
||||
// Mock cache.get to return previous result when requested
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
// Cache miss for fetch (get null, exists false) -> fetch fails -> null
|
||||
// getPreviousResult returns previous result for :previous_result key
|
||||
mockCache.get.mockImplementation(async (key: string) => {
|
||||
if (key.includes(":previous_result")) {
|
||||
return { ok: true, data: mockPreviousResult };
|
||||
}
|
||||
@@ -227,7 +228,7 @@ describe("License Core Logic", () => {
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
expect(mockCache.withCache).toHaveBeenCalled();
|
||||
expect(mockCache.get).toHaveBeenCalled();
|
||||
expect(license).toEqual({
|
||||
active: true,
|
||||
features: mockPreviousResult.features,
|
||||
@@ -250,11 +251,8 @@ describe("License Core Logic", () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
// Mock cache.withCache to return null (simulating fetch failure)
|
||||
mockCache.withCache.mockResolvedValue(null);
|
||||
|
||||
// Mock cache.get to return previous result when requested
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
// Cache miss -> fetch fails -> null; getPreviousResult returns old previous result
|
||||
mockCache.get.mockImplementation(async (key: string) => {
|
||||
if (key.includes(":previous_result")) {
|
||||
return { ok: true, data: mockPreviousResult };
|
||||
}
|
||||
@@ -265,7 +263,7 @@ describe("License Core Logic", () => {
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
expect(mockCache.withCache).toHaveBeenCalled();
|
||||
expect(mockCache.get).toHaveBeenCalled();
|
||||
expect(mockCache.set).toHaveBeenCalledWith(
|
||||
expect.stringContaining("fb:license:"),
|
||||
{
|
||||
@@ -319,12 +317,7 @@ describe("License Core Logic", () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Mock cache.withCache to return null (simulating fetch failure)
|
||||
mockCache.withCache.mockResolvedValue(null);
|
||||
|
||||
// Mock cache.get to return no previous result
|
||||
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
||||
|
||||
// Cache miss -> fetch fails; no previous result (default get returns null)
|
||||
fetch.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
@@ -397,49 +390,52 @@ describe("License Core Logic", () => {
|
||||
});
|
||||
expect(mockCache.get).not.toHaveBeenCalled();
|
||||
expect(mockCache.set).not.toHaveBeenCalled();
|
||||
expect(mockCache.withCache).not.toHaveBeenCalled();
|
||||
expect(mockCache.exists).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle fetch throwing an error and use grace period or return inactive", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Mock cache.withCache to return null (simulating fetch failure)
|
||||
mockCache.withCache.mockResolvedValue(null);
|
||||
|
||||
// Mock cache.get to return no previous result
|
||||
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
||||
|
||||
fetch.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
expect(license).toEqual({
|
||||
active: false,
|
||||
features: null,
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
status: "no-license" as const,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLicenseFeatures", () => {
|
||||
test("should return features if license is active", async () => {
|
||||
// Set up environment before import
|
||||
vi.stubGlobal("window", undefined);
|
||||
// Runs after "no-license" test which uses vi.doMock; env may have empty key
|
||||
vi.resetModules();
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
ENVIRONMENT: "production",
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
// Mock cache.withCache to return license details
|
||||
mockCache.withCache.mockResolvedValue({
|
||||
status: "active",
|
||||
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Cache miss -> fetch throws -> no previous result -> handleInitialFailure
|
||||
fetch.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
expect(license).toEqual({
|
||||
active: false,
|
||||
features: expect.objectContaining({
|
||||
isMultiOrgEnabled: false,
|
||||
projects: 3,
|
||||
removeBranding: false,
|
||||
}),
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
status: "unreachable" as const,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLicenseFeatures", () => {
|
||||
test("should return features if license is active", async () => {
|
||||
vi.resetModules();
|
||||
vi.stubGlobal("window", undefined);
|
||||
// Mock cache hit for fetchLicense (get returns license, exists true)
|
||||
const activeLicenseDetails = {
|
||||
status: "active" as const,
|
||||
features: {
|
||||
isMultiOrgEnabled: true,
|
||||
contacts: true,
|
||||
@@ -452,9 +448,22 @@ describe("License Core Logic", () => {
|
||||
spamProtection: true,
|
||||
ai: true,
|
||||
auditLogs: true,
|
||||
multiLanguageSurveys: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
},
|
||||
};
|
||||
mockCache.get.mockImplementation(async (key: string) => {
|
||||
if (key.includes(":previous_result")) {
|
||||
return { ok: true, data: null };
|
||||
}
|
||||
if (key.includes(":status")) {
|
||||
return { ok: true, data: activeLicenseDetails };
|
||||
}
|
||||
return { ok: true, data: null };
|
||||
});
|
||||
// Import after env and mocks are set
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
||||
|
||||
const { getLicenseFeatures } = await import("./license");
|
||||
const features = await getLicenseFeatures();
|
||||
expect(features).toEqual({
|
||||
@@ -469,14 +478,47 @@ describe("License Core Logic", () => {
|
||||
spamProtection: true,
|
||||
ai: true,
|
||||
auditLogs: true,
|
||||
multiLanguageSurveys: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should return null if license is inactive", async () => {
|
||||
const { getLicenseFeatures } = await import("./license");
|
||||
|
||||
// Mock cache.withCache to return expired license
|
||||
mockCache.withCache.mockResolvedValue({ status: "expired", features: null });
|
||||
// Mock cache hit with expired license (features can be default for schema)
|
||||
mockCache.get.mockImplementation(async (key: string) => {
|
||||
if (key.includes(":previous_result")) {
|
||||
return { ok: true, data: null };
|
||||
}
|
||||
if (key.includes(":status")) {
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
status: "expired",
|
||||
features: {
|
||||
isMultiOrgEnabled: false,
|
||||
projects: 3,
|
||||
twoFactorAuth: false,
|
||||
sso: false,
|
||||
whitelabel: false,
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
ai: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
auditLogs: false,
|
||||
multiLanguageSurveys: false,
|
||||
accessControl: false,
|
||||
quotas: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return { ok: true, data: null };
|
||||
});
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
||||
|
||||
const features = await getLicenseFeatures();
|
||||
expect(features).toBeNull();
|
||||
@@ -485,8 +527,8 @@ describe("License Core Logic", () => {
|
||||
test("should return null if getEnterpriseLicense throws", async () => {
|
||||
const { getLicenseFeatures } = await import("./license");
|
||||
|
||||
// Mock cache.withCache to throw an error
|
||||
mockCache.withCache.mockRejectedValue(new Error("Cache error"));
|
||||
// Mock cache.get to throw so getEnterpriseLicense fails
|
||||
mockCache.get.mockRejectedValue(new Error("Cache error"));
|
||||
|
||||
const features = await getLicenseFeatures();
|
||||
expect(features).toBeNull();
|
||||
@@ -499,23 +541,59 @@ describe("License Core Logic", () => {
|
||||
mockCache.get.mockReset();
|
||||
mockCache.set.mockReset();
|
||||
mockCache.del.mockReset();
|
||||
mockCache.withCache.mockReset();
|
||||
mockCache.exists.mockReset();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
test("should use 'browser' as cache key in browser environment", async () => {
|
||||
vi.stubGlobal("window", {});
|
||||
|
||||
// Set up default mock for cache.withCache
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
// Ensure env has license key (previous "no-license" test may have poisoned env)
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
ENVIRONMENT: "production",
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
// Cache miss so fetch runs; mock get/exists for cache checks
|
||||
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: false });
|
||||
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
status: "active",
|
||||
features: {
|
||||
isMultiOrgEnabled: true,
|
||||
projects: 5,
|
||||
twoFactorAuth: true,
|
||||
sso: true,
|
||||
whitelabel: true,
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
ai: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
multiLanguageSurveys: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as any);
|
||||
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
await getEnterpriseLicense();
|
||||
expect(mockCache.withCache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.stringContaining("fb:license:browser:status"),
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining("fb:license:browser:status"));
|
||||
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining("fb:license:browser:status"));
|
||||
});
|
||||
|
||||
test("should use 'no-license' as cache key when ENTERPRISE_LICENSE_KEY is not set", async () => {
|
||||
@@ -534,16 +612,19 @@ describe("License Core Logic", () => {
|
||||
await getEnterpriseLicense();
|
||||
// The cache should NOT be accessed if there is no license key
|
||||
expect(mockCache.get).not.toHaveBeenCalled();
|
||||
expect(mockCache.withCache).not.toHaveBeenCalled();
|
||||
expect(mockCache.exists).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use hashed license key as cache key when ENTERPRISE_LICENSE_KEY is set", async () => {
|
||||
vi.resetModules();
|
||||
const testLicenseKey = "test-license-key";
|
||||
vi.stubGlobal("window", undefined);
|
||||
|
||||
// Ensure env has license key (restore after "no-license" test)
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: testLicenseKey,
|
||||
ENVIRONMENT: "production",
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
@@ -551,22 +632,49 @@ describe("License Core Logic", () => {
|
||||
},
|
||||
}));
|
||||
|
||||
// Set up default mock for cache.withCache
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: false });
|
||||
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
status: "active",
|
||||
features: {
|
||||
isMultiOrgEnabled: true,
|
||||
projects: 5,
|
||||
twoFactorAuth: true,
|
||||
sso: true,
|
||||
whitelabel: true,
|
||||
removeBranding: true,
|
||||
contacts: true,
|
||||
ai: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
auditLogs: true,
|
||||
multiLanguageSurveys: true,
|
||||
accessControl: true,
|
||||
quotas: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as any);
|
||||
|
||||
const { hashString } = await import("@/lib/hash-string");
|
||||
const expectedHash = hashString(testLicenseKey);
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
await getEnterpriseLicense();
|
||||
expect(mockCache.withCache).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
expect.stringContaining(`fb:license:${expectedHash}:status`),
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(mockCache.get).toHaveBeenCalledWith(expect.stringContaining(`fb:license:${expectedHash}:status`));
|
||||
expect(mockCache.exists).toHaveBeenCalledWith(expect.stringContaining(`fb:license:${expectedHash}:status`));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error and Warning Logging", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
test("should log warning when setPreviousResult cache.set fails (line 176-178)", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
@@ -591,10 +699,19 @@ describe("License Core Logic", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// Mock successful fetch from API
|
||||
mockCache.withCache.mockResolvedValue(mockFetchedLicenseDetails);
|
||||
// Cache hit - fetchLicense returns cached license
|
||||
mockCache.get.mockImplementation(async (key: string) => {
|
||||
if (key.includes(":previous_result")) {
|
||||
return { ok: true, data: null };
|
||||
}
|
||||
if (key.includes(":status")) {
|
||||
return { ok: true, data: mockFetchedLicenseDetails };
|
||||
}
|
||||
return { ok: true, data: null };
|
||||
});
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: true });
|
||||
|
||||
// Mock cache.set to fail when saving previous result
|
||||
// cache.set fails when setPreviousResult tries to save (called for previous_result key)
|
||||
mockCache.set.mockResolvedValue({
|
||||
ok: false,
|
||||
error: new Error("Redis connection failed"),
|
||||
@@ -602,7 +719,6 @@ describe("License Core Logic", () => {
|
||||
|
||||
await getEnterpriseLicense();
|
||||
|
||||
// Verify that the warning was logged
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
{ error: new Error("Redis connection failed") },
|
||||
"Failed to cache previous result"
|
||||
@@ -613,10 +729,7 @@ describe("License Core Logic", () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Mock cache.withCache to execute the function (simulating cache miss)
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
|
||||
// Mock API response with 500 status
|
||||
// Cache miss -> fetch returns 500
|
||||
const mockStatus = 500;
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
@@ -626,7 +739,6 @@ describe("License Core Logic", () => {
|
||||
|
||||
await getEnterpriseLicense();
|
||||
|
||||
// Verify that the API error was logged with correct structure
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: mockStatus,
|
||||
@@ -641,8 +753,7 @@ describe("License Core Logic", () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Test with 403 Forbidden
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
// Cache miss -> fetch returns 403
|
||||
const mockStatus = 403;
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
@@ -652,7 +763,6 @@ describe("License Core Logic", () => {
|
||||
|
||||
await getEnterpriseLicense();
|
||||
|
||||
// Verify that the API error was logged with correct structure
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: mockStatus,
|
||||
@@ -675,8 +785,7 @@ describe("License Core Logic", () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
mockCache.withCache.mockResolvedValue(null);
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
mockCache.get.mockImplementation(async (key: string) => {
|
||||
if (key.includes(":previous_result")) {
|
||||
return { ok: true, data: mockPreviousResult };
|
||||
}
|
||||
@@ -687,7 +796,6 @@ describe("License Core Logic", () => {
|
||||
|
||||
await getEnterpriseLicense();
|
||||
|
||||
// Verify that the fallback info was logged
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fallbackLevel: "grace",
|
||||
@@ -712,8 +820,9 @@ describe("License Core Logic", () => {
|
||||
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
// Mock cache.withCache to execute the function (simulating cache miss)
|
||||
mockCache.withCache.mockImplementation(async (fn) => await fn());
|
||||
// Cache miss so fetchLicense fetches from server
|
||||
mockCache.get.mockResolvedValue({ ok: true, data: null });
|
||||
mockCache.exists.mockResolvedValue({ ok: true, data: false });
|
||||
|
||||
fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
|
||||
@@ -14,12 +14,14 @@ import { getInstanceId } from "@/lib/instance";
|
||||
import {
|
||||
TEnterpriseLicenseDetails,
|
||||
TEnterpriseLicenseFeatures,
|
||||
TEnterpriseLicenseStatusReturn,
|
||||
} from "@/modules/ee/license-check/types/enterprise-license";
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
CACHE: {
|
||||
FETCH_LICENSE_TTL_MS: 24 * 60 * 60 * 1000, // 24 hours
|
||||
FAILED_FETCH_TTL_MS: 10 * 60 * 1000, // 10 minutes for failed/null results
|
||||
PREVIOUS_RESULT_TTL_MS: 4 * 24 * 60 * 60 * 1000, // 4 days
|
||||
GRACE_PERIOD_MS: 3 * 24 * 60 * 60 * 1000, // 3 days
|
||||
MAX_RETRIES: 3,
|
||||
@@ -35,11 +37,14 @@ const CONFIG = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** TTL in ms for successful license fetch results (24h). Re-export for use in actions. */
|
||||
export const FETCH_LICENSE_TTL_MS = CONFIG.CACHE.FETCH_LICENSE_TTL_MS;
|
||||
/** TTL in ms for failed license fetch results (10 min). Re-export for use in actions. */
|
||||
export const FAILED_FETCH_TTL_MS = CONFIG.CACHE.FAILED_FETCH_TTL_MS;
|
||||
|
||||
// Types
|
||||
type FallbackLevel = "live" | "cached" | "grace" | "default";
|
||||
|
||||
type TEnterpriseLicenseStatusReturn = "active" | "expired" | "unreachable" | "no-license";
|
||||
|
||||
type TEnterpriseLicenseResult = {
|
||||
active: boolean;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
@@ -384,6 +389,12 @@ const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpri
|
||||
throw error;
|
||||
}
|
||||
logger.error(error, "Error while fetching license from server");
|
||||
logger.warn(
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
"License server fetch returned null - server may be unreachable"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -402,13 +413,35 @@ export const fetchLicense = async (): Promise<TEnterpriseLicenseDetails | null>
|
||||
}
|
||||
|
||||
fetchLicensePromise = (async () => {
|
||||
return await cache.withCache(
|
||||
async () => {
|
||||
return await fetchLicenseFromServerInternal();
|
||||
},
|
||||
getCacheKeys().FETCH_LICENSE_CACHE_KEY,
|
||||
CONFIG.CACHE.FETCH_LICENSE_TTL_MS
|
||||
);
|
||||
// Check cache first
|
||||
const cacheKey = getCacheKeys().FETCH_LICENSE_CACHE_KEY;
|
||||
const cached = await cache.get<TEnterpriseLicenseDetails | null>(cacheKey);
|
||||
const exists = await cache.exists(cacheKey);
|
||||
|
||||
// Only use cache if:
|
||||
// 1. Cache lookup succeeded
|
||||
// 2. Key exists in cache (distinguishes "not cached" from "cached null")
|
||||
// 3. Return cached.data (including null) - honors FAILED_FETCH_TTL_MS to suppress retries
|
||||
if (cached.ok && exists.ok && exists.data) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
// Cache miss -- fetch fresh
|
||||
const result = await fetchLicenseFromServerInternal();
|
||||
const ttl = result ? CONFIG.CACHE.FETCH_LICENSE_TTL_MS : CONFIG.CACHE.FAILED_FETCH_TTL_MS;
|
||||
|
||||
if (!result) {
|
||||
logger.warn(
|
||||
{
|
||||
ttlMinutes: Math.floor(ttl / 60000),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
"License fetch failed, caching null result with short TTL for faster retry"
|
||||
);
|
||||
}
|
||||
|
||||
await cache.set(getCacheKeys().FETCH_LICENSE_CACHE_KEY, result, ttl);
|
||||
return result;
|
||||
})();
|
||||
|
||||
fetchLicensePromise
|
||||
@@ -447,7 +480,6 @@ export const getEnterpriseLicense = reactCache(async (): Promise<TEnterpriseLice
|
||||
const currentTime = new Date();
|
||||
const [liveLicenseDetails, previousResult] = await Promise.all([fetchLicense(), getPreviousResult()]);
|
||||
const fallbackLevel = getFallbackLevel(liveLicenseDetails, previousResult, currentTime);
|
||||
|
||||
trackFallbackUsage(fallbackLevel);
|
||||
|
||||
let currentLicenseState: TPreviousResult | undefined;
|
||||
@@ -487,6 +519,16 @@ export const getEnterpriseLicense = reactCache(async (): Promise<TEnterpriseLice
|
||||
if (!validateFallback(previousResult)) {
|
||||
return await handleInitialFailure(currentTime);
|
||||
}
|
||||
logger.warn(
|
||||
{
|
||||
lastChecked: previousResult.lastChecked.toISOString(),
|
||||
gracePeriodEnds: new Date(
|
||||
previousResult.lastChecked.getTime() + CONFIG.CACHE.GRACE_PERIOD_MS
|
||||
).toISOString(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
"License server unreachable, using grace period. Will retry in ~10 minutes."
|
||||
);
|
||||
const graceResult: TEnterpriseLicenseResult = {
|
||||
active: previousResult.active,
|
||||
features: previousResult.features,
|
||||
@@ -542,4 +584,27 @@ export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures |
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear license fetch cache (but preserve previous result cache for grace period)
|
||||
* Used by the recheck license action to force a fresh fetch without losing grace period
|
||||
*/
|
||||
export const clearLicenseCache = async (): Promise<void> => {
|
||||
memoryCache = null;
|
||||
const cacheKeys = getCacheKeys();
|
||||
// Only clear the main fetch cache, NOT the previous result cache
|
||||
// This preserves the grace period fallback if the server is unreachable
|
||||
const delResult = await cache.del([cacheKeys.FETCH_LICENSE_CACHE_KEY]);
|
||||
if (!delResult.ok) {
|
||||
logger.warn({ error: delResult.error }, "Failed to delete license cache");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch license directly from server without using cache
|
||||
* Used by the recheck license action for a fresh check
|
||||
*/
|
||||
export const fetchLicenseFresh = async (): Promise<TEnterpriseLicenseDetails | null> => {
|
||||
return await fetchLicenseFromServerInternal();
|
||||
};
|
||||
|
||||
// All permission checking functions and their helpers have been moved to utils.ts
|
||||
|
||||
@@ -29,3 +29,5 @@ export const ZEnterpriseLicenseDetails = z.object({
|
||||
});
|
||||
|
||||
export type TEnterpriseLicenseDetails = z.infer<typeof ZEnterpriseLicenseDetails>;
|
||||
|
||||
export type TEnterpriseLicenseStatusReturn = "active" | "expired" | "unreachable" | "no-license";
|
||||
|
||||
Reference in New Issue
Block a user