Compare commits

...

1 Commits

Author SHA1 Message Date
pandeymangg
0233541287 adds security signup UI in the org settings 2025-12-25 13:23:07 +05:30
19 changed files with 323 additions and 5 deletions

View File

@@ -1099,6 +1099,13 @@ checksums:
environments/settings/teams/please_fill_all_project_fields: 6712059df63c432ecd31f3c52b8e4d87
environments/settings/teams/read: 2494ca23d10e5b6381eb271aceeb5270
environments/settings/teams/read_write: 278a90dade128198d4c93ac00c345320
environments/settings/teams/security_updates_description: 17c49b565a7dde28b810f67af2e8db07
environments/settings/teams/security_updates_enroll: edcc8815899ece9209ce981c26c44df3
environments/settings/teams/security_updates_enrolled: 98863ec2d846b7a13ff1ed38ce1038fe
environments/settings/teams/security_updates_enrolled_description: d9c7605767af8f4d7265cba7dfba5f11
environments/settings/teams/security_updates_enrolled_successfully: 3bbb41fac1c04effec3af8ffbd8b72c5
environments/settings/teams/security_updates_enrolling: 15ca7daa32fb57e18a0a6357de26eb4b
environments/settings/teams/security_updates_title: 2f5f5f55bb9a325b5c8228bcad4f2784
environments/settings/teams/select_member: 7f4a38312aabbbe3fe92756b57bd5d75
environments/settings/teams/select_project: 6e4f4a24178660851d9ae0874706be9f
environments/settings/teams/team_admin: 5df68214685738029af678ae1d5912bb

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Bitte fülle alle Felder aus, um ein neues Projekt hinzuzufügen.",
"read": "Lesen",
"read_write": "Lesen & Schreiben",
"security_updates_description": "Melden Sie sich für unsere Sicherheits-Mailingliste an, um informiert zu bleiben, falls Sicherheitslücken gefunden werden.",
"security_updates_enroll": "Jetzt anmelden",
"security_updates_enrolled": "Angemeldet",
"security_updates_enrolled_description": "Sie sind angemeldet, um Sicherheitsupdates unter {email} zu erhalten.",
"security_updates_enrolled_successfully": "Erfolgreich für Sicherheitsupdates angemeldet!",
"security_updates_enrolling": "Wird angemeldet...",
"security_updates_title": "Sicherheitsupdates",
"select_member": "Mitglied auswählen",
"select_project": "Projekt auswählen",
"team_admin": "Team-Admin",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Please fill all the fields to add a new project.",
"read": "Read",
"read_write": "Read & Write",
"security_updates_description": "Enroll to our Security Mailing List to stay informed if vulnerabilities are found.",
"security_updates_enroll": "Enroll now",
"security_updates_enrolled": "Enrolled",
"security_updates_enrolled_description": "You're enrolled to receive security updates at {email}.",
"security_updates_enrolled_successfully": "Successfully enrolled for security updates!",
"security_updates_enrolling": "Enrolling...",
"security_updates_title": "Security Updates",
"select_member": "Select member",
"select_project": "Select project",
"team_admin": "Team Admin",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Por favor, rellena todos los campos para añadir un nuevo proyecto.",
"read": "Lectura",
"read_write": "Lectura y escritura",
"security_updates_description": "Inscríbete en nuestra lista de correo de seguridad para mantenerte informado si se encuentran vulnerabilidades.",
"security_updates_enroll": "Inscribirse ahora",
"security_updates_enrolled": "Inscrito",
"security_updates_enrolled_description": "Estás inscrito para recibir actualizaciones de seguridad en {email}.",
"security_updates_enrolled_successfully": "Te has inscrito correctamente para recibir actualizaciones de seguridad.",
"security_updates_enrolling": "Inscribiendo...",
"security_updates_title": "Actualizaciones de seguridad",
"select_member": "Seleccionar miembro",
"select_project": "Seleccionar proyecto",
"team_admin": "Administrador de equipo",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Veuillez remplir tous les champs pour ajouter un nouveau projet.",
"read": "Lire",
"read_write": "Lire et Écrire",
"security_updates_description": "Inscrivez-vous à notre liste de diffusion sécurité pour être informé si des vulnérabilités sont découvertes.",
"security_updates_enroll": "S'inscrire maintenant",
"security_updates_enrolled": "Inscrit",
"security_updates_enrolled_description": "Vous êtes inscrit pour recevoir les mises à jour de sécurité à {email}.",
"security_updates_enrolled_successfully": "Inscription aux mises à jour de sécurité réussie!",
"security_updates_enrolling": "Inscription en cours...",
"security_updates_title": "Mises à jour de sécurité",
"select_member": "Sélectionner membre",
"select_project": "Sélectionner projet",
"team_admin": "Administrateur d'équipe",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "新しいプロジェクトを追加するには、すべてのフィールドを記入してください。",
"read": "読み取り",
"read_write": "読み書き",
"security_updates_description": "脆弱性が発見された際に通知を受け取るため、セキュリティメーリングリストに登録してください。",
"security_updates_enroll": "今すぐ登録",
"security_updates_enrolled": "登録済み",
"security_updates_enrolled_description": "{email}でセキュリティアップデートを受信するよう登録されています。",
"security_updates_enrolled_successfully": "セキュリティアップデートの登録が完了しました",
"security_updates_enrolling": "登録中...",
"security_updates_title": "セキュリティアップデート",
"select_member": "メンバーを選択",
"select_project": "プロジェクトを選択",
"team_admin": "チーム管理者",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Vul alle velden in om een nieuw project toe te voegen.",
"read": "Lezen",
"read_write": "Lezen en schrijven",
"security_updates_description": "Schrijf je in voor onze beveiligingsmailinglijst om op de hoogte te blijven als er kwetsbaarheden worden gevonden.",
"security_updates_enroll": "Nu inschrijven",
"security_updates_enrolled": "Ingeschreven",
"security_updates_enrolled_description": "Je bent ingeschreven om beveiligingsupdates te ontvangen op {email}.",
"security_updates_enrolled_successfully": "Succesvol ingeschreven voor beveiligingsupdates!",
"security_updates_enrolling": "Bezig met inschrijven...",
"security_updates_title": "Beveiligingsupdates",
"select_member": "Selecteer lid",
"select_project": "Selecteer project",
"team_admin": "Teambeheerder",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.",
"read": "Leitura",
"read_write": "Leitura & Escrita",
"security_updates_description": "Inscreva-se na nossa lista de e-mails de segurança para ser informado caso vulnerabilidades sejam encontradas.",
"security_updates_enroll": "Inscrever-se agora",
"security_updates_enrolled": "Inscrito",
"security_updates_enrolled_description": "Você está inscrito para receber atualizações de segurança em {email}.",
"security_updates_enrolled_successfully": "Inscrito com sucesso para atualizações de segurança!",
"security_updates_enrolling": "Inscrevendo...",
"security_updates_title": "Atualizações de segurança",
"select_member": "Selecionar membro",
"select_project": "Selecionar projeto",
"team_admin": "Administrador da equipe",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.",
"read": "Ler",
"read_write": "Ler e Escrever",
"security_updates_description": "Inscreva-se na nossa lista de correio de segurança para se manter informado caso sejam encontradas vulnerabilidades.",
"security_updates_enroll": "Inscrever agora",
"security_updates_enrolled": "Inscrito",
"security_updates_enrolled_description": "Está inscrito para receber atualizações de segurança em {email}.",
"security_updates_enrolled_successfully": "Inscrito com sucesso para atualizações de segurança!",
"security_updates_enrolling": "A inscrever...",
"security_updates_title": "Atualizações de segurança",
"select_member": "Selecionar membro",
"select_project": "Selecionar projeto",
"team_admin": "Administrador da Equipa",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Vă rugăm să completați toate câmpurile pentru a adăuga un proiect nou.",
"read": "Citește",
"read_write": "Citire & Scriere",
"security_updates_description": "Înscrie-te la lista noastră de e-mailuri de securitate pentru a fi informat dacă sunt descoperite vulnerabilități.",
"security_updates_enroll": "Înscrie-te acum",
"security_updates_enrolled": "Înscris",
"security_updates_enrolled_description": "Ești înscris pentru a primi actualizări de securitate la {email}.",
"security_updates_enrolled_successfully": "Înscriere reușită pentru actualizările de securitate!",
"security_updates_enrolling": "Se înscrie...",
"security_updates_title": "Actualizări de securitate",
"select_member": "Selectează membrul",
"select_project": "Selectează proiectul",
"team_admin": "Administrator Echipe",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Пожалуйста, заполните все поля для добавления нового проекта.",
"read": "Чтение",
"read_write": "Чтение и запись",
"security_updates_description": "Подпишитесь на нашу рассылку по безопасности, чтобы получать уведомления о найденных уязвимостях.",
"security_updates_enroll": "Подписаться",
"security_updates_enrolled": "Подписка оформлена",
"security_updates_enrolled_description": "Вы подписаны на получение обновлений безопасности на адрес {email}.",
"security_updates_enrolled_successfully": "Вы успешно подписались на обновления безопасности!",
"security_updates_enrolling": "Оформляется подписка...",
"security_updates_title": "Обновления безопасности",
"select_member": "Выберите участника",
"select_project": "Выберите проект",
"team_admin": "Администратор команды",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "Vänligen fyll i alla fält för att lägga till ett nytt projekt.",
"read": "Läs",
"read_write": "Läs och skriv",
"security_updates_description": "Anmäl dig till vår säkerhetsmejllista för att hålla dig informerad om sårbarheter upptäcks.",
"security_updates_enroll": "Anmäl dig nu",
"security_updates_enrolled": "Anmäld",
"security_updates_enrolled_description": "Du är anmäld för att ta emot säkerhetsuppdateringar på {email}.",
"security_updates_enrolled_successfully": "Du har anmälts för säkerhetsuppdateringar!",
"security_updates_enrolling": "Anmäler...",
"security_updates_title": "Säkerhetsuppdateringar",
"select_member": "Välj medlem",
"select_project": "Välj projekt",
"team_admin": "Teamadministratör",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "请 填写 所有 字段 以 添加 新 项目。",
"read": "阅读",
"read_write": "读 & 写",
"security_updates_description": "加入我们的安全邮件列表,及时了解发现的安全漏洞信息。",
"security_updates_enroll": "立即加入",
"security_updates_enrolled": "已加入",
"security_updates_enrolled_description": "您已加入安全更新通知,相关信息将发送至 {email}。",
"security_updates_enrolled_successfully": "已成功加入安全更新通知!",
"security_updates_enrolling": "正在加入...",
"security_updates_title": "安全更新",
"select_member": "选择成员",
"select_project": "选择项目",
"team_admin": "团队管理员",

View File

@@ -1180,6 +1180,13 @@
"please_fill_all_project_fields": "請填寫所有欄位以新增新專案。",
"read": "讀取",
"read_write": "讀取和寫入",
"security_updates_description": "加入我們的安全郵件名單,隨時掌握漏洞相關資訊。",
"security_updates_enroll": "立即加入",
"security_updates_enrolled": "已加入",
"security_updates_enrolled_description": "您已加入安全更新通知,將會寄送至 {email}。",
"security_updates_enrolled_successfully": "已成功加入安全更新通知!",
"security_updates_enrolling": "正在加入...",
"security_updates_title": "安全更新",
"select_member": "選擇成員",
"select_project": "選擇專案",
"team_admin": "團隊管理員",

View File

@@ -24,6 +24,7 @@ import {
getOrganizationOwnerCount,
} from "@/modules/organization/settings/teams/lib/membership";
import { deleteInvite, getInvite, inviteUser, resendInvite } from "./lib/invite";
import { enrollInSecurityUpdates } from "./lib/security-updates";
const ZDeleteInviteAction = z.object({
inviteId: ZUuid,
@@ -387,3 +388,39 @@ export const leaveOrganizationAction = authenticatedActionClient.schema(ZLeaveOr
}
)
);
const ZEnrollSecurityUpdatesAction = z.object({
organizationId: ZId,
});
export const enrollSecurityUpdatesAction = authenticatedActionClient
.schema(ZEnrollSecurityUpdatesAction)
.action(async ({ ctx, parsedInput }) => {
// Ensure this is only called for self-hosted instances
if (IS_FORMBRICKS_CLOUD) {
throw new OperationNotAllowedError(
"Security updates enrollment is only available for self-hosted instances"
);
}
// Only owners can enroll in security updates
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: parsedInput.organizationId,
access: [
{
type: "organization",
roles: ["owner"],
},
],
});
// Enroll with the current user's email
const result = await enrollInSecurityUpdates(ctx.user.email);
if (!result.success) {
throw new Error("Failed to enroll in security updates");
}
return { success: true };
});

View File

@@ -0,0 +1,98 @@
"use client";
import { ShieldCheckIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/cn";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { enrollSecurityUpdatesAction } from "@/modules/organization/settings/teams/actions";
import { TSecurityUpdatesStatus } from "@/modules/organization/settings/teams/lib/security-updates";
import { Button } from "@/modules/ui/components/button";
import { H4, P } from "@/modules/ui/components/typography";
interface SecurityUpdatesCardProps {
organizationId: string;
userEmail: string;
securityUpdatesStatus: TSecurityUpdatesStatus;
}
export const SecurityUpdatesCard = ({
organizationId,
userEmail,
securityUpdatesStatus,
}: SecurityUpdatesCardProps) => {
const router = useRouter();
const { t } = useTranslation();
const [isEnrolling, setIsEnrolling] = useState(false);
const handleEnroll = async () => {
setIsEnrolling(true);
try {
const result = await enrollSecurityUpdatesAction({ organizationId });
if (result?.data?.success) {
toast.success(t("environments.settings.teams.security_updates_enrolled_successfully"));
router.refresh();
} else {
const errorMessage = getFormattedErrorMessage(result);
toast.error(errorMessage);
}
} catch (error) {
toast.error(t("common.something_went_wrong_please_try_again"));
console.error(error);
} finally {
setIsEnrolling(false);
}
};
const isEnrolled = securityUpdatesStatus.enrolled;
return (
<div
className={cn(
"relative my-4 w-full max-w-4xl rounded-xl border bg-white shadow-sm",
isEnrolled ? "border-green-200 bg-green-50" : "border-slate-200"
)}>
<div className="flex items-start justify-between p-6">
<div className="flex items-start gap-4">
<div
className={cn(
"flex h-10 w-10 items-center justify-center rounded-full",
isEnrolled ? "bg-green-100" : "bg-slate-100"
)}>
<ShieldCheckIcon className={cn("h-5 w-5", isEnrolled ? "text-green-600" : "text-slate-600")} />
</div>
<div className="flex flex-col gap-1">
<H4 className="font-medium tracking-normal">
{t("environments.settings.teams.security_updates_title")}
</H4>
<P className="!mt-0 text-sm text-slate-500">
{isEnrolled
? t("environments.settings.teams.security_updates_enrolled_description", {
email: securityUpdatesStatus.email || userEmail,
})
: t("environments.settings.teams.security_updates_description")}
</P>
</div>
</div>
{!isEnrolled && (
<Button onClick={handleEnroll} disabled={isEnrolling} className="shrink-0">
{isEnrolling
? t("environments.settings.teams.security_updates_enrolling")
: t("environments.settings.teams.security_updates_enroll")}
</Button>
)}
{isEnrolled && (
<div className="flex items-center gap-2 rounded-full bg-green-100 px-3 py-1">
<div className="h-2 w-2 rounded-full bg-green-500" />
<span className="text-sm font-medium text-green-700">
{t("environments.settings.teams.security_updates_enrolled")}
</span>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,68 @@
"use server";
import { getInstanceId } from "@/lib/instance";
export type TSecurityUpdatesStatus = {
enrolled: boolean;
email?: string;
};
/**
* Checks if the current instance is enrolled in security updates.
*
* TODO: Replace with actual EE server call
* GET /security-updates/status?instanceId=xxx
*
* @returns The enrollment status and email if enrolled
*/
export const getSecurityUpdatesStatus = async (): Promise<TSecurityUpdatesStatus> => {
const instanceId = await getInstanceId();
if (!instanceId) {
return { enrolled: false };
}
// TODO: Replace with actual EE server call
// const response = await fetch(`${EE_SERVER_URL}/instances/${instanceId}/security-updates`);
// if (!response.ok) {
// return { enrolled: false };
// }
// return await response.json();
// Mock: Always return not enrolled for now
return { enrolled: false };
};
/**
* Enrolls the current instance in security updates.
*
* TODO: Replace with actual EE server call
* POST /security-updates/enroll { instanceId, email }
*
* @param email - The email address to receive security updates
* @returns Success status
*/
export const enrollInSecurityUpdates = async (email: string): Promise<{ success: boolean }> => {
const instanceId = await getInstanceId();
if (!instanceId) {
throw new Error("Instance ID not found");
}
// TODO: Replace with actual EE server call
// const response = await fetch(`${EE_SERVER_URL}/instances/${instanceId}/security-updates`, {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ instanceId, email }),
// });
//
// if (!response.ok) {
// throw new Error("Failed to enroll in security updates");
// }
//
// return await response.json();
// Mock: Always succeed for now
console.log(`[Mock] Enrolling instance ${instanceId} with email ${email}`);
return { success: true };
};

View File

@@ -1,20 +1,25 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constants";
import { getUserManagementAccess } from "@/lib/membership/utils";
import { getUser } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { MembersView } from "@/modules/organization/settings/teams/components/members-view";
import { SecurityUpdatesCard } from "@/modules/organization/settings/teams/components/security-updates-card";
import { getSecurityUpdatesStatus } from "@/modules/organization/settings/teams/lib/security-updates";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
export const TeamsPage = async (props) => {
export const TeamsPage = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslate();
const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
const { session, currentUserMembership, organization, isOwner } = await getEnvironmentAuth(
params.environmentId
);
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
@@ -32,6 +37,12 @@ export const TeamsPage = async (props) => {
const hasUserManagementAccess =
hasStandardUserManagementAccess || (isAccessControlAllowed && isTeamAdminUser);
// Fetch security updates status for self-hosted instances only (owners only)
const shouldShowSecurityUpdates = !IS_FORMBRICKS_CLOUD && isOwner;
const [securityUpdatesStatus, user] = shouldShowSecurityUpdates
? await Promise.all([getSecurityUpdatesStatus(), getUser(session.user.id)])
: [null, null];
return (
<PageContentWrapper>
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
@@ -42,6 +53,15 @@ export const TeamsPage = async (props) => {
activeId="teams"
/>
</PageHeader>
{securityUpdatesStatus && user && (
<SecurityUpdatesCard
organizationId={organization.id}
userEmail={user.email}
securityUpdatesStatus={securityUpdatesStatus}
/>
)}
<MembersView
membershipRole={currentUserMembership?.role}
organization={organization}

View File

@@ -442,7 +442,4 @@ const sentryOptions = {
// Runtime Sentry reporting still depends on DSN being set via environment variables
const exportConfig = process.env.SENTRY_AUTH_TOKEN ? withSentryConfig(nextConfig, sentryOptions) : nextConfig;
console.log("BASE PATH", nextConfig.basePath);
export default exportConfig;