Explorar el Código

fix(clients): reject spaces, '/', '\' and control chars in subscription ID

Like the client email, the subId is embedded directly in subscription
URLs, so the same characters break it. Validate it on the backend
(Create + Update) and the frontend (Zod), with a localized message
across all 13 locales. An empty subId stays allowed (it is then
auto-generated).
MHSanaei hace 19 horas
padre
commit
2fa7be86dc

+ 3 - 3
frontend/src/schemas/client.ts

@@ -119,7 +119,7 @@ export const GroupSummarySchema = z.object({
 
 export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []);
 
-export function emailHasForbiddenChars(value: string): boolean {
+export function hasForbiddenClientChars(value: string): boolean {
   if (value.includes('/') || value.includes('\\') || value.includes(' ')) return true;
   for (let i = 0; i < value.length; i++) {
     const code = value.charCodeAt(i);
@@ -133,8 +133,8 @@ export const ClientFormSchema = z.object({
     .string()
     .trim()
     .min(1, 'pages.clients.email')
-    .refine((v) => !emailHasForbiddenChars(v), 'pages.clients.emailInvalidChars'),
-  subId: z.string(),
+    .refine((v) => !hasForbiddenClientChars(v), 'pages.clients.emailInvalidChars'),
+  subId: z.string().refine((v) => !hasForbiddenClientChars(v), 'pages.clients.subIdInvalidChars'),
   uuid: z.string(),
   password: z.string(),
   auth: z.string(),

+ 23 - 3
web/service/client.go

@@ -408,12 +408,26 @@ type ClientCreatePayload struct {
 	InboundIds []int        `json:"inboundIds"`
 }
 
-func validateClientEmail(email string) error {
-	for _, r := range email {
+func hasForbiddenClientChar(s string) bool {
+	for _, r := range s {
 		if r == '/' || r == '\\' || r == ' ' || r < 0x20 || r == 0x7f {
-			return common.NewError("client email contains an invalid character:", email)
+			return true
 		}
 	}
+	return false
+}
+
+func validateClientEmail(email string) error {
+	if hasForbiddenClientChar(email) {
+		return common.NewError("client email contains an invalid character:", email)
+	}
+	return nil
+}
+
+func validateClientSubID(subID string) error {
+	if hasForbiddenClientChar(subID) {
+		return common.NewError("client subId contains an invalid character:", subID)
+	}
 	return nil
 }
 
@@ -428,6 +442,9 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
 	if err := validateClientEmail(client.Email); err != nil {
 		return false, err
 	}
+	if err := validateClientSubID(client.SubID); err != nil {
+		return false, err
+	}
 	if len(payload.InboundIds) == 0 {
 		return false, common.NewError("at least one inbound is required")
 	}
@@ -596,6 +613,9 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
 	if err := validateClientEmail(updated.Email); err != nil {
 		return false, err
 	}
+	if err := validateClientSubID(updated.SubID); err != nil {
+		return false, err
+	}
 	if updated.SubID == "" {
 		updated.SubID = existing.SubID
 	}

+ 25 - 0
web/service/client_email_validation_test.go

@@ -30,3 +30,28 @@ func TestValidateClientEmail(t *testing.T) {
 		}
 	}
 }
+
+func TestValidateClientSubID(t *testing.T) {
+	valid := []string{
+		"",
+		"abc123",
+		"sub-id_value",
+	}
+	for _, subID := range valid {
+		if err := validateClientSubID(subID); err != nil {
+			t.Errorf("validateClientSubID(%q) = %v, want nil", subID, err)
+		}
+	}
+
+	invalid := []string{
+		"a/b",
+		"with space",
+		"back\\slash",
+		"new\nline",
+	}
+	for _, subID := range invalid {
+		if err := validateClientSubID(subID); err == nil {
+			t.Errorf("validateClientSubID(%q) = nil, want error", subID)
+		}
+	}
+}

+ 1 - 0
web/translation/ar-EG.json

@@ -647,6 +647,7 @@
       "online": "متصل",
       "email": "البريد",
       "emailInvalidChars": "لا يمكن أن يحتوي البريد الإلكتروني على مسافات أو '/' أو '\\' أو أحرف تحكم",
+      "subIdInvalidChars": "لا يمكن أن يحتوي معرّف الاشتراك على مسافات أو '/' أو '\\' أو أحرف تحكم",
       "group": "المجموعة",
       "groupDesc": "تسمية منطقية لتجميع العملاء (مثل فريق، عميل، منطقة). يمكن تصفيتها من شريط الأدوات.",
       "groupPlaceholder": "مثلاً customer-a",

+ 1 - 0
web/translation/en-US.json

@@ -647,6 +647,7 @@
       "online": "Online",
       "email": "Email",
       "emailInvalidChars": "Email cannot contain spaces, '/', '\\', or control characters",
+      "subIdInvalidChars": "Subscription ID cannot contain spaces, '/', '\\', or control characters",
       "group": "Group",
       "groupDesc": "Logical label used to bucket related clients (e.g. team, customer, region). Filterable from the toolbar.",
       "groupPlaceholder": "e.g. customer-a",

+ 1 - 0
web/translation/es-ES.json

@@ -647,6 +647,7 @@
       "online": "En línea",
       "email": "Email",
       "emailInvalidChars": "El correo no puede contener espacios, '/', '\\' ni caracteres de control",
+      "subIdInvalidChars": "El ID de suscripción no puede contener espacios, '/', '\\' ni caracteres de control",
       "group": "Grupo",
       "groupDesc": "Etiqueta lógica para agrupar clientes relacionados (p. ej. equipo, cliente, región). Filtrable desde la barra de herramientas.",
       "groupPlaceholder": "p. ej. customer-a",

+ 1 - 0
web/translation/fa-IR.json

@@ -647,6 +647,7 @@
       "online": "آنلاین",
       "email": "ایمیل",
       "emailInvalidChars": "ایمیل نمی‌تواند شامل فاصله، '/'، '\\' یا کاراکترهای کنترلی باشد",
+      "subIdInvalidChars": "شناسه‌ی اشتراک نمی‌تواند شامل فاصله، '/'، '\\' یا کاراکترهای کنترلی باشد",
       "group": "گروه",
       "groupDesc": "برچسبی منطقی برای دسته‌بندی کاربران مرتبط (مثل تیم، مشتری، منطقه). از نوار ابزار قابل فیلتر است.",
       "groupPlaceholder": "مثلاً customer-a",

+ 1 - 0
web/translation/id-ID.json

@@ -647,6 +647,7 @@
       "online": "Online",
       "email": "Email",
       "emailInvalidChars": "Email tidak boleh mengandung spasi, '/', '\\', atau karakter kontrol",
+      "subIdInvalidChars": "ID langganan tidak boleh mengandung spasi, '/', '\\', atau karakter kontrol",
       "group": "Grup",
       "groupDesc": "Label logis untuk mengelompokkan klien terkait (mis. tim, pelanggan, wilayah). Dapat difilter dari toolbar.",
       "groupPlaceholder": "mis. customer-a",

+ 1 - 0
web/translation/ja-JP.json

@@ -647,6 +647,7 @@
       "online": "オンライン",
       "email": "メール",
       "emailInvalidChars": "メールアドレスにスペース、'/'、'\\'、または制御文字を含めることはできません",
+      "subIdInvalidChars": "サブスクリプションIDにスペース、'/'、'\\'、または制御文字を含めることはできません",
       "group": "グループ",
       "groupDesc": "関連クライアントをまとめる論理ラベル(チーム、顧客、地域など)。ツールバーからフィルタ可能。",
       "groupPlaceholder": "例: customer-a",

+ 1 - 0
web/translation/pt-BR.json

@@ -647,6 +647,7 @@
       "online": "Online",
       "email": "Email",
       "emailInvalidChars": "O e-mail não pode conter espaços, '/', '\\' ou caracteres de controle",
+      "subIdInvalidChars": "O ID de assinatura não pode conter espaços, '/', '\\' ou caracteres de controle",
       "group": "Grupo",
       "groupDesc": "Rótulo lógico para agrupar clientes relacionados (ex.: equipe, cliente, região). Filtrável pela barra de ferramentas.",
       "groupPlaceholder": "ex.: customer-a",

+ 1 - 0
web/translation/ru-RU.json

@@ -647,6 +647,7 @@
       "online": "В сети",
       "email": "Email",
       "emailInvalidChars": "Email не может содержать пробелы, '/', '\\' или управляющие символы",
+      "subIdInvalidChars": "ID подписки не может содержать пробелы, '/', '\\' или управляющие символы",
       "group": "Группа",
       "groupDesc": "Логическая метка для группировки связанных клиентов (например, команда, клиент, регион). Фильтруется из панели инструментов.",
       "groupPlaceholder": "например, customer-a",

+ 1 - 0
web/translation/tr-TR.json

@@ -647,6 +647,7 @@
       "online": "Çevrimiçi",
       "email": "Email",
       "emailInvalidChars": "E-posta boşluk, '/', '\\' veya kontrol karakterleri içeremez",
+      "subIdInvalidChars": "Abonelik kimliği boşluk, '/', '\\' veya kontrol karakterleri içeremez",
       "group": "Grup",
       "groupDesc": "İlgili istemcileri gruplamak için mantıksal etiket (ekip, müşteri, bölge). Araç çubuğundan filtrelenebilir.",
       "groupPlaceholder": "örn. customer-a",

+ 1 - 0
web/translation/uk-UA.json

@@ -647,6 +647,7 @@
       "online": "У мережі",
       "email": "Email",
       "emailInvalidChars": "Email не може містити пробіли, '/', '\\' або керуючі символи",
+      "subIdInvalidChars": "ID підписки не може містити пробіли, '/', '\\' або керуючі символи",
       "group": "Група",
       "groupDesc": "Логічна мітка для групування пов'язаних клієнтів (напр. команда, клієнт, регіон). Фільтрується з панелі інструментів.",
       "groupPlaceholder": "напр. customer-a",

+ 1 - 0
web/translation/vi-VN.json

@@ -647,6 +647,7 @@
       "online": "Trực tuyến",
       "email": "Email",
       "emailInvalidChars": "Email không được chứa khoảng trắng, '/', '\\' hoặc ký tự điều khiển",
+      "subIdInvalidChars": "ID đăng ký không được chứa khoảng trắng, '/', '\\' hoặc ký tự điều khiển",
       "group": "Nhóm",
       "groupDesc": "Nhãn logic để gom các client liên quan (nhóm, khách hàng, khu vực). Có thể lọc từ thanh công cụ.",
       "groupPlaceholder": "ví dụ customer-a",

+ 1 - 0
web/translation/zh-CN.json

@@ -647,6 +647,7 @@
       "online": "在线",
       "email": "邮箱",
       "emailInvalidChars": "邮箱不能包含空格、'/'、'\\' 或控制字符",
+      "subIdInvalidChars": "订阅ID不能包含空格、'/'、'\\' 或控制字符",
       "group": "分组",
       "groupDesc": "用于对相关客户端进行分桶的逻辑标签(如团队、客户、地区)。可从工具栏筛选。",
       "groupPlaceholder": "如 customer-a",

+ 1 - 0
web/translation/zh-TW.json

@@ -647,6 +647,7 @@
       "online": "上線",
       "email": "電子郵件",
       "emailInvalidChars": "電子郵件不能包含空格、'/'、'\\' 或控制字元",
+      "subIdInvalidChars": "訂閱ID不能包含空格、'/'、'\\' 或控制字元",
       "group": "群組",
       "groupDesc": "用於將相關客戶端歸類的邏輯標籤(如團隊、客戶、地區)。可從工具列篩選。",
       "groupPlaceholder": "如 customer-a",