mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-18 09:52:05 -05:00
feat: security signup ui (#7088)
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { ShieldCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const SecurityListTip = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="flex items-center space-x-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm">
|
||||
<ShieldCheckIcon className="h-5 w-5 flex-shrink-0 text-blue-400" />
|
||||
<p className="text-sm">
|
||||
{t("environments.settings.general.security_list_tip")}{" "}
|
||||
<Link
|
||||
href="https://formbricks.com/security#stay-informed-with-formbricks-security-updates"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-blue-700">
|
||||
{t("environments.settings.general.security_list_tip_link")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
||||
import { SecurityListTip } from "./components/SecurityListTip";
|
||||
|
||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
@@ -48,6 +49,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{!IS_FORMBRICKS_CLOUD && <SecurityListTip />}
|
||||
<SettingsCard
|
||||
title={t("environments.settings.general.organization_name")}
|
||||
description={t("environments.settings.general.organization_name_description")}>
|
||||
|
||||
@@ -61,6 +61,10 @@ checksums:
|
||||
auth/signup/password_validation_uppercase_and_lowercase: ae98b485024dbff1022f6048e22443cd
|
||||
auth/signup/please_verify_captcha: 12938ca7ca13e3f933737dd5436fa1c0
|
||||
auth/signup/privacy_policy: 7459744a63ef8af4e517a09024bd7c08
|
||||
auth/signup/product_updates_description: f20eedb2cf42d2235b1fe0294086695b
|
||||
auth/signup/product_updates_title: 31e099ba18abb0a49f8a75fece1f1791
|
||||
auth/signup/security_updates_description: 4643df07f13cec619e7fd91c8f14d93b
|
||||
auth/signup/security_updates_title: de5127f5847cdd412906607e1402f48d
|
||||
auth/signup/terms_of_service: 5add91f519e39025708e54a7eb7a9fc5
|
||||
auth/signup/title: 96addc349f834eaa5d14c786d5478b1c
|
||||
auth/signup_without_verification_success/user_successfully_created: ff849ebedc5dacb36493d7894f16edc7
|
||||
@@ -954,6 +958,8 @@ checksums:
|
||||
environments/settings/general/remove_logo: f60f1803e6fc8017b1eae7c30089107f
|
||||
environments/settings/general/replace_logo: e3c8bec7574a670607e88771164e272f
|
||||
environments/settings/general/resend_invitation_email: 6305d1ffa015c377ef59fe9c2661cf02
|
||||
environments/settings/general/security_list_tip: 0bbed89fa5265da7e07767087f87c736
|
||||
environments/settings/general/security_list_tip_link: ccdb1a21610ebf5a626d813b155be4ba
|
||||
environments/settings/general/share_invite_link: b40b7ffbcf02d7464be52fb562df5e3a
|
||||
environments/settings/general/share_this_link_to_let_your_organization_member_join_your_organization: 6eb43d5b1c855572b7ab35f527ba953c
|
||||
environments/settings/general/test_email_sent_successfully: aa68214f5e0707c9615e01343640ab32
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Mix aus Groß- und Kleinbuchstaben",
|
||||
"please_verify_captcha": "Bitte bestätige reCAPTCHA",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"product_updates_description": "Monatliche Produktneuigkeiten und Feature-Updates, es gilt die Datenschutzerklärung.",
|
||||
"product_updates_title": "Produkt-Updates",
|
||||
"security_updates_description": "Nur sicherheitsrelevante Informationen, es gilt die Datenschutzerklärung.",
|
||||
"security_updates_title": "Sicherheits-Updates",
|
||||
"terms_of_service": "Nutzungsbedingungen",
|
||||
"title": "Erstelle dein Formbricks-Konto"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Logo entfernen",
|
||||
"replace_logo": "Logo ersetzen",
|
||||
"resend_invitation_email": "Einladungsemail erneut senden",
|
||||
"security_list_tip": "Haben Sie sich für unsere Sicherheitsliste angemeldet? Bleiben Sie informiert, um Ihre Instanz sicher zu halten!",
|
||||
"security_list_tip_link": "Hier registrieren.",
|
||||
"share_invite_link": "Einladungslink teilen",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Teile diesen Link, damit dein Organisationsmitglied deiner Organisation beitreten kann:",
|
||||
"test_email_sent_successfully": "Test-E-Mail erfolgreich gesendet",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Mix of uppercase and lowercase",
|
||||
"please_verify_captcha": "Please verify reCAPTCHA",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"product_updates_description": "Monthly product news and feature updates, Privacy Policy applies.",
|
||||
"product_updates_title": "Product updates",
|
||||
"security_updates_description": "Security relevant information only, Privacy Policy applies.",
|
||||
"security_updates_title": "Security updates",
|
||||
"terms_of_service": "Terms of Service",
|
||||
"title": "Create your Formbricks account"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Remove logo",
|
||||
"replace_logo": "Replace logo",
|
||||
"resend_invitation_email": "Resend Invitation Email",
|
||||
"security_list_tip": "Are you signed up for our Security List? Stay informed to keep your instance secure!",
|
||||
"security_list_tip_link": "Sign up here.",
|
||||
"share_invite_link": "Share Invite Link",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Share this link to let your organization member join your organization:",
|
||||
"test_email_sent_successfully": "Test email sent successfully",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Mezcla de mayúsculas y minúsculas",
|
||||
"please_verify_captcha": "Por favor, verifica el reCAPTCHA",
|
||||
"privacy_policy": "Política de privacidad",
|
||||
"product_updates_description": "Noticias mensuales del producto y actualizaciones de funciones, se aplica la política de privacidad.",
|
||||
"product_updates_title": "Actualizaciones del producto",
|
||||
"security_updates_description": "Solo información relevante sobre seguridad, se aplica la política de privacidad.",
|
||||
"security_updates_title": "Actualizaciones de seguridad",
|
||||
"terms_of_service": "Términos de servicio",
|
||||
"title": "Crea tu cuenta de Formbricks"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Eliminar logotipo",
|
||||
"replace_logo": "Reemplazar logotipo",
|
||||
"resend_invitation_email": "Reenviar correo electrónico de invitación",
|
||||
"security_list_tip": "¿Estás suscrito a nuestra lista de seguridad? ¡Mantente informado para mantener tu instancia segura!",
|
||||
"security_list_tip_link": "Regístrate aquí.",
|
||||
"share_invite_link": "Compartir enlace de invitación",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Comparte este enlace para permitir que los miembros de tu organización se unan a tu organización:",
|
||||
"test_email_sent_successfully": "Correo electrónico de prueba enviado correctamente",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Mélange de majuscules et de minuscules",
|
||||
"please_verify_captcha": "Veuillez vérifier reCAPTCHA",
|
||||
"privacy_policy": "Politique de confidentialité",
|
||||
"product_updates_description": "Actualités mensuelles du produit et mises à jour des fonctionnalités, la politique de confidentialité s'applique.",
|
||||
"product_updates_title": "Mises à jour du produit",
|
||||
"security_updates_description": "Informations relatives à la sécurité uniquement, la politique de confidentialité s'applique.",
|
||||
"security_updates_title": "Mises à jour de sécurité",
|
||||
"terms_of_service": "Conditions d'utilisation",
|
||||
"title": "Créez votre compte Formbricks"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Supprimer le logo",
|
||||
"replace_logo": "Remplacer le logo",
|
||||
"resend_invitation_email": "Renvoyer l'e-mail d'invitation",
|
||||
"security_list_tip": "Êtes-vous inscrit à notre liste de sécurité ? Restez informé pour maintenir votre instance sécurisée !",
|
||||
"security_list_tip_link": "Inscrivez-vous ici.",
|
||||
"share_invite_link": "Partager le lien d'invitation",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Partagez ce lien pour permettre à un membre de votre organisation de rejoindre votre organisation :",
|
||||
"test_email_sent_successfully": "E-mail de test envoyé avec succès",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "大文字と小文字を混ぜる",
|
||||
"please_verify_captcha": "reCAPTCHAを認証してください",
|
||||
"privacy_policy": "プライバシーポリシー",
|
||||
"product_updates_description": "毎月の製品ニュースと機能アップデート、プライバシーポリシーが適用されます。",
|
||||
"product_updates_title": "製品アップデート",
|
||||
"security_updates_description": "セキュリティ関連情報のみ、プライバシーポリシーが適用されます。",
|
||||
"security_updates_title": "セキュリティアップデート",
|
||||
"terms_of_service": "利用規約",
|
||||
"title": "Formbricksアカウントを作成"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "ロゴを削除",
|
||||
"replace_logo": "ロゴを交換",
|
||||
"resend_invitation_email": "招待メールを再送信",
|
||||
"security_list_tip": "セキュリティリストに登録していますか?インスタンスを安全に保つために最新情報を入手しましょう!",
|
||||
"security_list_tip_link": "こちらからサインアップしてください。",
|
||||
"share_invite_link": "招待リンクを共有",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "このリンクを共有して、組織メンバーを招待できます:",
|
||||
"test_email_sent_successfully": "テストメールを正常に送信しました",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Mix van hoofdletters en kleine letters",
|
||||
"please_verify_captcha": "Controleer reCAPTCHA",
|
||||
"privacy_policy": "Privacybeleid",
|
||||
"product_updates_description": "Maandelijks productnieuws en feature-updates, privacybeleid is van toepassing.",
|
||||
"product_updates_title": "Product-updates",
|
||||
"security_updates_description": "Alleen beveiligingsrelevante informatie, privacybeleid is van toepassing.",
|
||||
"security_updates_title": "Beveiligingsupdates",
|
||||
"terms_of_service": "Servicevoorwaarden",
|
||||
"title": "Maak uw Formbricks-account aan"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Logo verwijderen",
|
||||
"replace_logo": "Logo vervangen",
|
||||
"resend_invitation_email": "Uitnodigings-e-mail opnieuw verzenden",
|
||||
"security_list_tip": "Ben je aangemeld voor onze beveiligingslijst? Blijf op de hoogte om je instantie veilig te houden!",
|
||||
"security_list_tip_link": "Meld je hier aan.",
|
||||
"share_invite_link": "Deel de uitnodigingslink",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Deel deze link om uw organisatielid lid te laten worden van uw organisatie:",
|
||||
"test_email_sent_successfully": "Test-e-mail succesvol verzonden",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "mistura de maiúsculas e minúsculas",
|
||||
"please_verify_captcha": "Por favor, verifique o reCAPTCHA",
|
||||
"privacy_policy": "Política de Privacidade",
|
||||
"product_updates_description": "Novidades mensais do produto e atualizações de recursos, a Política de Privacidade se aplica.",
|
||||
"product_updates_title": "Atualizações do produto",
|
||||
"security_updates_description": "Apenas informações relevantes sobre segurança, a Política de Privacidade se aplica.",
|
||||
"security_updates_title": "Atualizações de segurança",
|
||||
"terms_of_service": "Termos de Serviço",
|
||||
"title": "Crie sua conta no Formbricks"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Remover logo",
|
||||
"replace_logo": "Substituir logo",
|
||||
"resend_invitation_email": "Reenviar E-mail de Convite",
|
||||
"security_list_tip": "Você está inscrito na nossa Lista de Segurança? Mantenha-se informado para manter sua instância segura!",
|
||||
"security_list_tip_link": "Cadastre-se aqui.",
|
||||
"share_invite_link": "Compartilhar Link de Convite",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Compartilhe esse link para que o membro da sua organização possa entrar na sua organização:",
|
||||
"test_email_sent_successfully": "E-mail de teste enviado com sucesso",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Mistura de maiúsculas e minúsculas",
|
||||
"please_verify_captcha": "Por favor, verifique o reCAPTCHA",
|
||||
"privacy_policy": "Política de Privacidade",
|
||||
"product_updates_description": "Notícias mensais sobre o produto e atualizações de funcionalidades, aplica-se a Política de Privacidade.",
|
||||
"product_updates_title": "Atualizações do produto",
|
||||
"security_updates_description": "Apenas informações relevantes sobre segurança, aplica-se a Política de Privacidade.",
|
||||
"security_updates_title": "Atualizações de segurança",
|
||||
"terms_of_service": "Termos de Serviço",
|
||||
"title": "Crie a sua conta Formbricks"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Remover logótipo",
|
||||
"replace_logo": "Substituir logotipo",
|
||||
"resend_invitation_email": "Reenviar Email de Convite",
|
||||
"security_list_tip": "Está inscrito na nossa Lista de Segurança? Mantenha-se informado para manter a sua instância segura!",
|
||||
"security_list_tip_link": "Inscreva-se aqui.",
|
||||
"share_invite_link": "Partilhar Link de Convite",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Partilhe este link para permitir que o membro da sua organização se junte à sua organização:",
|
||||
"test_email_sent_successfully": "Email de teste enviado com sucesso",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Amestec de majuscule și minuscule",
|
||||
"please_verify_captcha": "Vă rugăm să verificați CAPTCHA",
|
||||
"privacy_policy": "Politica de confidențialitate",
|
||||
"product_updates_description": "Noutăți lunare despre produse și actualizări de funcționalități; se aplică Politica de confidențialitate.",
|
||||
"product_updates_title": "Actualizări de produs",
|
||||
"security_updates_description": "Doar informații relevante pentru securitate; se aplică Politica de confidențialitate.",
|
||||
"security_updates_title": "Actualizări de securitate",
|
||||
"terms_of_service": "Termeni de utilizare a serviciului",
|
||||
"title": "Creați-vă contul Formbricks"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Înlătură siglă",
|
||||
"replace_logo": "Înlocuiește sigla",
|
||||
"resend_invitation_email": "Retrimite emailul de invitație",
|
||||
"security_list_tip": "Ești abonat la lista noastră de securitate? Rămâi informat pentru a-ți menține instanța în siguranță!",
|
||||
"security_list_tip_link": "Înscrie-te aici.",
|
||||
"share_invite_link": "Distribuie link-ul de invitație",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Distribuie acest link pentru a permite membrului organizației să se alăture organizației tale:",
|
||||
"test_email_sent_successfully": "Email de test trimis cu succes",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Сочетание заглавных и строчных букв",
|
||||
"please_verify_captcha": "Пожалуйста, подтвердите reCAPTCHA",
|
||||
"privacy_policy": "Политика конфиденциальности",
|
||||
"product_updates_description": "Ежемесячные новости о продукте и обновления функций. Применяется Политика конфиденциальности.",
|
||||
"product_updates_title": "Обновления продукта",
|
||||
"security_updates_description": "Только важная информация по безопасности. Применяется Политика конфиденциальности.",
|
||||
"security_updates_title": "Обновления безопасности",
|
||||
"terms_of_service": "Условия использования",
|
||||
"title": "Создайте аккаунт Formbricks"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Удалить логотип",
|
||||
"replace_logo": "Заменить логотип",
|
||||
"resend_invitation_email": "Отправить приглашение повторно",
|
||||
"security_list_tip": "Вы подписаны на нашу рассылку по безопасности? Будьте в курсе, чтобы обезопасить свой экземпляр!",
|
||||
"security_list_tip_link": "Зарегистрируйтесь здесь.",
|
||||
"share_invite_link": "Поделиться ссылкой-приглашением",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Поделитесь этой ссылкой, чтобы участник вашей организации мог присоединиться к ней:",
|
||||
"test_email_sent_successfully": "Тестовое письмо успешно отправлено",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "Blandning av stora och små bokstäver",
|
||||
"please_verify_captcha": "Vänligen verifiera reCAPTCHA",
|
||||
"privacy_policy": "Integritetspolicy",
|
||||
"product_updates_description": "Månatliga produktnyheter och funktionsuppdateringar. Integritetspolicyn gäller.",
|
||||
"product_updates_title": "Produktuppdateringar",
|
||||
"security_updates_description": "Endast säkerhetsrelaterad information. Integritetspolicyn gäller.",
|
||||
"security_updates_title": "Säkerhetsuppdateringar",
|
||||
"terms_of_service": "Användarvillkor",
|
||||
"title": "Skapa ditt Formbricks-konto"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "Ta bort logotyp",
|
||||
"replace_logo": "Ersätt logotyp",
|
||||
"resend_invitation_email": "Skicka inbjudningsmejl igen",
|
||||
"security_list_tip": "Är du med på vår säkerhetslista? Håll dig informerad för att skydda din instans!",
|
||||
"security_list_tip_link": "Registrera dig här.",
|
||||
"share_invite_link": "Dela inbjudningslänk",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "Dela denna länk för att låta din organisationsmedlem gå med i din organisation:",
|
||||
"test_email_sent_successfully": "Test-e-post skickat",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "大小写混合",
|
||||
"please_verify_captcha": "请 验证 reCAPTCHA",
|
||||
"privacy_policy": "隐私政策",
|
||||
"product_updates_description": "每月产品新闻和功能更新,适用隐私政策。",
|
||||
"product_updates_title": "产品更新",
|
||||
"security_updates_description": "仅限安全相关信息,适用隐私政策。",
|
||||
"security_updates_title": "安全更新",
|
||||
"terms_of_service": "服务条款",
|
||||
"title": "创建你的 Formbricks 账户"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "移除 logo",
|
||||
"replace_logo": "替换 logo",
|
||||
"resend_invitation_email": "重新发送邀请邮件",
|
||||
"security_list_tip": "您已订阅我们的安全列表了吗?保持关注,保障您的实例安全!",
|
||||
"security_list_tip_link": "点击此处注册。",
|
||||
"share_invite_link": "分享邀请链接",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "分享 这个 链接 以 让 你的 组织 成员 加入 你的 组织:",
|
||||
"test_email_sent_successfully": "测试 邮件 发送 成功",
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
"password_validation_uppercase_and_lowercase": "混合使用大小寫字母",
|
||||
"please_verify_captcha": "請驗證 reCAPTCHA",
|
||||
"privacy_policy": "隱私權政策",
|
||||
"product_updates_description": "每月產品新聞與功能更新,適用隱私權政策。",
|
||||
"product_updates_title": "產品更新",
|
||||
"security_updates_description": "僅限安全相關資訊,適用隱私權政策。",
|
||||
"security_updates_title": "安全更新",
|
||||
"terms_of_service": "服務條款",
|
||||
"title": "建立您的 Formbricks 帳戶"
|
||||
},
|
||||
@@ -1015,6 +1019,8 @@
|
||||
"remove_logo": "移除標誌",
|
||||
"replace_logo": "取代標誌",
|
||||
"resend_invitation_email": "重新發送邀請電子郵件",
|
||||
"security_list_tip": "您已訂閱我們的安全名單了嗎?保持關注,確保您的實例安全!",
|
||||
"security_list_tip_link": "請在此註冊。",
|
||||
"share_invite_link": "分享邀請連結",
|
||||
"share_this_link_to_let_your_organization_member_join_your_organization": "分享此連結以讓您的組織成員加入您的組織:",
|
||||
"test_email_sent_successfully": "測試電子郵件已成功發送",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { applyIPRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { subscribeUserToMailingList } from "@/modules/ee/mailing/lib/mailing-subscription";
|
||||
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/modules/email";
|
||||
|
||||
const ZCreatedUser = ZUser.pick({
|
||||
@@ -44,6 +45,9 @@ const ZCreateUserAction = z.object({
|
||||
(token) => !IS_TURNSTILE_CONFIGURED || (IS_TURNSTILE_CONFIGURED && token),
|
||||
"CAPTCHA verification required"
|
||||
),
|
||||
isFormbricksCloud: z.boolean(),
|
||||
subscribeToSecurityUpdates: z.boolean().optional(),
|
||||
subscribeToProductUpdates: z.boolean().optional(),
|
||||
});
|
||||
|
||||
async function verifyTurnstileIfConfigured(turnstileToken: string | undefined): Promise<void> {
|
||||
@@ -191,6 +195,13 @@ export const createUserAction = actionClient.schema(ZCreateUserAction).action(
|
||||
parsedInput.inviteToken,
|
||||
parsedInput.emailVerificationDisabled
|
||||
);
|
||||
|
||||
await subscribeUserToMailingList({
|
||||
email: user.email,
|
||||
isFormbricksCloud: parsedInput.isFormbricksCloud,
|
||||
subscribeToSecurityUpdates: parsedInput.subscribeToSecurityUpdates,
|
||||
subscribeToProductUpdates: parsedInput.subscribeToProductUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { createUserAction } from "@/modules/auth/signup/actions";
|
||||
import { TermsPrivacyLinks } from "@/modules/auth/signup/components/terms-privacy-links";
|
||||
import { SSOOptions } from "@/modules/ee/sso/components/sso-options";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { PasswordInput } from "@/modules/ui/components/password-input";
|
||||
@@ -48,6 +49,7 @@ interface SignupFormProps {
|
||||
samlTenant: string;
|
||||
samlProduct: string;
|
||||
turnstileSiteKey?: string;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export const SignupForm = ({
|
||||
@@ -69,6 +71,7 @@ export const SignupForm = ({
|
||||
samlTenant,
|
||||
samlProduct,
|
||||
turnstileSiteKey,
|
||||
isFormbricksCloud,
|
||||
}: SignupFormProps) => {
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
@@ -76,6 +79,8 @@ export const SignupForm = ({
|
||||
const inviteToken = searchParams?.get("inviteToken");
|
||||
const router = useRouter();
|
||||
const [turnstileToken, setTurnstileToken] = useState<string>();
|
||||
const [subscribeToSecurityUpdates, setSubscribeToSecurityUpdates] = useState(false);
|
||||
const [subscribeToProductUpdates, setSubscribeToProductUpdates] = useState(false);
|
||||
|
||||
const turnstile = useTurnstile();
|
||||
|
||||
@@ -110,6 +115,9 @@ export const SignupForm = ({
|
||||
inviteToken: inviteToken ?? "",
|
||||
emailVerificationDisabled,
|
||||
turnstileToken,
|
||||
isFormbricksCloud,
|
||||
subscribeToSecurityUpdates,
|
||||
subscribeToProductUpdates,
|
||||
});
|
||||
|
||||
const emailTokenActionResponse = await createEmailTokenAction({ email: data.email });
|
||||
@@ -239,6 +247,43 @@ export const SignupForm = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{showLogin &&
|
||||
(isFormbricksCloud ? (
|
||||
<label
|
||||
htmlFor="product-updates"
|
||||
className="my-4 flex cursor-pointer space-x-2 rounded-md border border-slate-200 bg-slate-100 p-2 text-left">
|
||||
<Checkbox
|
||||
id="product-updates"
|
||||
checked={subscribeToProductUpdates}
|
||||
onCheckedChange={(checked) => setSubscribeToProductUpdates(checked === true)}
|
||||
className="mt-0.5 h-4 w-4"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-700">
|
||||
{t("auth.signup.product_updates_title")}
|
||||
</span>
|
||||
<p className="text-xs text-slate-500">{t("auth.signup.product_updates_description")}</p>
|
||||
</div>
|
||||
</label>
|
||||
) : (
|
||||
<label
|
||||
htmlFor="security-updates"
|
||||
className="my-4 flex cursor-pointer space-x-2 rounded-md border border-slate-200 bg-slate-100 p-2 text-left">
|
||||
<Checkbox
|
||||
id="security-updates"
|
||||
checked={subscribeToSecurityUpdates}
|
||||
onCheckedChange={(checked) => setSubscribeToSecurityUpdates(checked === true)}
|
||||
className="mt-0.5 h-4 w-4"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-700">
|
||||
{t("auth.signup.security_updates_title")}
|
||||
</span>
|
||||
<p className="text-xs text-slate-500">{t("auth.signup.security_updates_description")}</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
|
||||
{showLogin && (
|
||||
<Button
|
||||
data-testid="signup-submit"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EMAIL_VERIFICATION_DISABLED,
|
||||
GITHUB_OAUTH_ENABLED,
|
||||
GOOGLE_OAUTH_ENABLED,
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
IS_TURNSTILE_CONFIGURED,
|
||||
OIDC_DISPLAY_NAME,
|
||||
OIDC_OAUTH_ENABLED,
|
||||
@@ -76,6 +77,7 @@ export const SignupPage = async ({ searchParams: searchParamsProps }) => {
|
||||
samlTenant={SAML_TENANT}
|
||||
samlProduct={SAML_PRODUCT}
|
||||
turnstileSiteKey={TURNSTILE_SITE_KEY}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
</FormWrapper>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ const CONFIG = {
|
||||
env.ENVIRONMENT === "staging"
|
||||
? "https://staging.ee.formbricks.com/api/licenses/check"
|
||||
: "https://ee.formbricks.com/api/licenses/check",
|
||||
// ENDPOINT: "https://localhost:8080/api/licenses/check",
|
||||
TIMEOUT_MS: 5000,
|
||||
},
|
||||
} as const;
|
||||
|
||||
205
apps/web/modules/ee/mailing/lib/mailing-subscription.test.ts
Normal file
205
apps/web/modules/ee/mailing/lib/mailing-subscription.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { subscribeToMailingList, subscribeUserToMailingList } from "./mailing-subscription";
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/validate", () => ({
|
||||
validateInputs: vi.fn(),
|
||||
}));
|
||||
|
||||
globalThis.fetch = vi.fn();
|
||||
|
||||
describe("subscribeToMailingList", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("should successfully subscribe to security mailing list", async () => {
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||
|
||||
const result = await subscribeToMailingList({
|
||||
email: "test@example.com",
|
||||
listId: "security",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
"https://ee.formbricks.com/api/v1/public/mailing/security/subscriptions",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: "test@example.com" }),
|
||||
})
|
||||
);
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
{ listId: "security" },
|
||||
"Successfully subscribed to security mailing list"
|
||||
);
|
||||
});
|
||||
|
||||
test("should successfully subscribe to product-updates mailing list", async () => {
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||
|
||||
const result = await subscribeToMailingList({
|
||||
email: "test@example.com",
|
||||
listId: "product-updates",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
"https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: "test@example.com" }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should return error when API returns non-ok response", async () => {
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||
new Response("Bad Request", { status: 400, statusText: "Bad Request" })
|
||||
);
|
||||
|
||||
const result = await subscribeToMailingList({
|
||||
email: "test@example.com",
|
||||
listId: "security",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false, error: "Failed to subscribe: 400" });
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ status: 400, error: "Bad Request" },
|
||||
"Failed to subscribe to security mailing list"
|
||||
);
|
||||
});
|
||||
|
||||
test("should return error when fetch throws an error", async () => {
|
||||
vi.mocked(globalThis.fetch).mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const result = await subscribeToMailingList({
|
||||
email: "test@example.com",
|
||||
listId: "security",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false, error: "Failed to subscribe to mailing list" });
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
"Error subscribing to security mailing list"
|
||||
);
|
||||
});
|
||||
|
||||
test("should return timeout error when request times out", async () => {
|
||||
const abortError = new Error("Aborted");
|
||||
abortError.name = "AbortError";
|
||||
vi.mocked(globalThis.fetch).mockRejectedValueOnce(abortError);
|
||||
|
||||
const result = await subscribeToMailingList({
|
||||
email: "test@example.com",
|
||||
listId: "security",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: false, error: "Request timed out" });
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ listId: "security" },
|
||||
"Mailing subscription request timed out"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("subscribeUserToMailingList", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should subscribe to product-updates when isFormbricksCloud is true and subscribeToProductUpdates is true", async () => {
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||
|
||||
await subscribeUserToMailingList({
|
||||
email: "test@example.com",
|
||||
isFormbricksCloud: true,
|
||||
subscribeToProductUpdates: true,
|
||||
subscribeToSecurityUpdates: false,
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
"https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test("should not subscribe when isFormbricksCloud is true but subscribeToProductUpdates is false", async () => {
|
||||
await subscribeUserToMailingList({
|
||||
email: "test@example.com",
|
||||
isFormbricksCloud: true,
|
||||
subscribeToProductUpdates: false,
|
||||
subscribeToSecurityUpdates: true,
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should subscribe to security when isFormbricksCloud is false and subscribeToSecurityUpdates is true", async () => {
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||
|
||||
await subscribeUserToMailingList({
|
||||
email: "test@example.com",
|
||||
isFormbricksCloud: false,
|
||||
subscribeToSecurityUpdates: true,
|
||||
subscribeToProductUpdates: false,
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
"https://ee.formbricks.com/api/v1/public/mailing/security/subscriptions",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test("should not subscribe when isFormbricksCloud is false but subscribeToSecurityUpdates is false", async () => {
|
||||
await subscribeUserToMailingList({
|
||||
email: "test@example.com",
|
||||
isFormbricksCloud: false,
|
||||
subscribeToSecurityUpdates: false,
|
||||
subscribeToProductUpdates: true,
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should not subscribe when both subscription flags are undefined", async () => {
|
||||
await subscribeUserToMailingList({
|
||||
email: "test@example.com",
|
||||
isFormbricksCloud: true,
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should prioritize product-updates for cloud users even if security is also true", async () => {
|
||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(new Response(null, { status: 200 }));
|
||||
|
||||
await subscribeUserToMailingList({
|
||||
email: "test@example.com",
|
||||
isFormbricksCloud: true,
|
||||
subscribeToProductUpdates: true,
|
||||
subscribeToSecurityUpdates: true,
|
||||
});
|
||||
|
||||
// Should only call product-updates endpoint for cloud users
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
"https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
91
apps/web/modules/ee/mailing/lib/mailing-subscription.ts
Normal file
91
apps/web/modules/ee/mailing/lib/mailing-subscription.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
"use server";
|
||||
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TUserEmail, ZUserEmail } from "@formbricks/types/user";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
|
||||
export type TMailingListId = "security" | "product-updates";
|
||||
|
||||
const MAILING_LIST_ENDPOINTS: Record<TMailingListId, string> = {
|
||||
security: "https://ee.formbricks.com/api/v1/public/mailing/security/subscriptions",
|
||||
"product-updates": "https://ee.formbricks.com/api/v1/public/mailing/product-updates/subscriptions",
|
||||
} as const;
|
||||
|
||||
const EE_SERVER_TIMEOUT_MS = 5000;
|
||||
|
||||
interface TSubscribeToMailingListParams {
|
||||
email: TUserEmail;
|
||||
listId: TMailingListId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe a user to a mailing list via the EE server
|
||||
* @param email - The user's email address
|
||||
* @param listId - The mailing list ID ("security" or "product-updates")
|
||||
*/
|
||||
export const subscribeToMailingList = async ({
|
||||
email,
|
||||
listId,
|
||||
}: TSubscribeToMailingListParams): Promise<{ success: boolean; error?: string }> => {
|
||||
validateInputs([email, ZUserEmail.toLowerCase()]);
|
||||
|
||||
const endpoint = MAILING_LIST_ENDPOINTS[listId];
|
||||
if (!endpoint) {
|
||||
logger.error({ listId }, "Invalid mailing list ID");
|
||||
return { success: false, error: "Invalid mailing list ID" };
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), EE_SERVER_TIMEOUT_MS);
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.error(
|
||||
{ status: response.status, error: errorText },
|
||||
`Failed to subscribe to ${listId} mailing list`
|
||||
);
|
||||
return { success: false, error: `Failed to subscribe: ${response.status}` };
|
||||
}
|
||||
|
||||
logger.info({ listId }, `Successfully subscribed to ${listId} mailing list`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
logger.error({ listId }, "Mailing subscription request timed out");
|
||||
return { success: false, error: "Request timed out" };
|
||||
}
|
||||
|
||||
logger.error(error, `Error subscribing to ${listId} mailing list`);
|
||||
return { success: false, error: "Failed to subscribe to mailing list" };
|
||||
}
|
||||
};
|
||||
|
||||
export const subscribeUserToMailingList = async ({
|
||||
email,
|
||||
isFormbricksCloud,
|
||||
subscribeToSecurityUpdates,
|
||||
subscribeToProductUpdates,
|
||||
}: {
|
||||
email: TUserEmail;
|
||||
isFormbricksCloud: boolean;
|
||||
subscribeToSecurityUpdates?: boolean;
|
||||
subscribeToProductUpdates?: boolean;
|
||||
}): Promise<void> => {
|
||||
if (isFormbricksCloud && subscribeToProductUpdates) {
|
||||
await subscribeToMailingList({ email, listId: "product-updates" });
|
||||
} else if (!isFormbricksCloud && subscribeToSecurityUpdates) {
|
||||
await subscribeToMailingList({ email, listId: "security" });
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EMAIL_VERIFICATION_DISABLED,
|
||||
GITHUB_OAUTH_ENABLED,
|
||||
GOOGLE_OAUTH_ENABLED,
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
IS_TURNSTILE_CONFIGURED,
|
||||
OIDC_DISPLAY_NAME,
|
||||
OIDC_OAUTH_ENABLED,
|
||||
@@ -57,6 +58,7 @@ export const SignupPage = async () => {
|
||||
samlTenant={SAML_TENANT}
|
||||
samlProduct={SAML_PRODUCT}
|
||||
turnstileSiteKey={TURNSTILE_SITE_KEY}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"db:push": "prisma db push --accept-data-loss",
|
||||
"db:seed": "dotenv -e ../../.env -- tsx src/seed.ts",
|
||||
"db:seed:clear": "dotenv -e ../../.env -- tsx src/seed.ts --clear",
|
||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev && pnpm db:seed",
|
||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
||||
"db:start": "pnpm db:setup",
|
||||
"format": "prisma format",
|
||||
"generate": "prisma generate",
|
||||
|
||||
Reference in New Issue
Block a user