From 31af7036a32d75d382e77de84d24938ce8353413 Mon Sep 17 00:00:00 2001 From: smanylov Date: Sat, 10 Jan 2026 15:12:51 +0700 Subject: [PATCH] update validation --- src/app/actions/auth.ts | 12 +- src/app/actions/definitions.ts | 124 ++++------ src/app/lib/validation-config-parser.ts | 225 ++++++++++++++++++ .../app/lib/validation-config.json | 12 +- src/app/ui/forms/register-form.tsx | 11 +- src/i18n/messages/en.json | 14 +- src/i18n/messages/ru.json | 14 +- 7 files changed, 316 insertions(+), 96 deletions(-) create mode 100644 src/app/lib/validation-config-parser.ts rename validation-rules.json => src/app/lib/validation-config.json (87%) diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index 7611d48..e84c52c 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -1,6 +1,6 @@ 'use server' -import { SignupFormSchema, loginFormSchema, API_BASE_URL } from '@/app/actions/definitions'; +import { SignupFormSchema, loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions'; import { createSession, deleteSession, getSessionData } from '@/app/actions/session'; import { redirect } from 'next/navigation'; @@ -154,7 +154,9 @@ export async function registration( const companyName = formData.get('companyName') as string || ''; const agree = formData.get('agree') as string || ''; - const validatedFields = SignupFormSchema.safeParse({ + const SignupFormSchema = await getSignupFormSchema(); + + const dataToValidate = { fullName, email, phone: phone.replace(/[-\(\)\s]/g, ''), @@ -162,7 +164,9 @@ export async function registration( confirm_password, agree, companyName - }); + }; + + const validatedFields = SignupFormSchema.safeParse(dataToValidate); if (!validatedFields.success) { const errors: Record = {} @@ -219,7 +223,7 @@ export async function registration( message_code: string }; if (parsed.message_desc === 'Operation successful') { - await createSession(parsed.message_body.token, email); + await createSession(parsed.message_body.token, email as string); } else { throw parsed; } diff --git a/src/app/actions/definitions.ts b/src/app/actions/definitions.ts index aa024fe..cbd260d 100644 --- a/src/app/actions/definitions.ts +++ b/src/app/actions/definitions.ts @@ -1,4 +1,6 @@ import * as z from 'zod' +import { createValidationSchema } from "@/app/lib/validation-config-parser"; +import validationConfig from '@/app/lib/validation-config.json'; export type FormState = | { @@ -16,80 +18,56 @@ export type FormAction = ( formData: FormData ) => Promise -const emailRegex = /^([a-z0-9.\-_]+|[а-яё0-9.\-_]+)@(([a-z0-9.\-]+)\.([a-z]{2,})|([a-z0-9.\-]+)\.([а-яё]{2,})|([а-яё0-9.\-]+)\.([a-z]{2,})|([а-яё0-9.\-]+)\.([а-яё]{2,}))$/i; +interface FieldConfig { + maxSize?: { value: number; message: string }; + minSize?: { value: number; message: string }; + allowedSymbols?: { value: string; message: string }; + optional?: { value: boolean }; + requiredDigit?: { value: boolean; message: string }; + requiredLetter?: { value: boolean; message: string }; + caseType?: { value: 'LOWER' | 'UPPER' | 'ANY'; message: string }; + emailValidation?: { value: boolean; message: string }; +} + +interface ValidationConfig { + [key: string]: FieldConfig; +} + +const defaultConfig = validationConfig as ValidationConfig; + +let cachedSignupSchema: any = null; +let schemaPromise: Promise | null = null; + +async function loadConfigFromBackend(): Promise { + try { + const response = await fetch('/api/validation-config'); + if (!response.ok) throw new Error('Failed to load config'); + return await response.json(); + } catch (error) { + console.error('Using default validation config:', error); + return defaultConfig; + } +} + +export async function getSignupFormSchema() { + console.log('get scheme'); + if (cachedSignupSchema) { + return cachedSignupSchema; + } + + if (!schemaPromise) { + schemaPromise = (async () => { + const config = await loadConfigFromBackend(); + cachedSignupSchema = createValidationSchema(config); + return cachedSignupSchema; + })(); + } + + return await schemaPromise; +} + +export const SignupFormSchema = createValidationSchema(defaultConfig); -export const SignupFormSchema = z - .object({ - fullName: z - .string() - .min(2, { error: 'register-error-min-name' }) - .max(100, { error: 'register-error-max-name' }) - .regex(/^[a-zA-Zа-яА-ЯёЁ\s\-'.]*$/, { message: 'register-error-name-not-allowed-symbols' }) - .trim(), - email: z - .string() - .trim() - .max(254, { error: 'register-error-max-email' }) - .refine( - (value) => value === value.toLowerCase(), - { message: 'register-error-email-no-uppercase' } - ) - .refine( - (value) => emailRegex.test(value), - { message: 'register-error-email-invalid' } - ), - phone: z - .string() - .min(12, { error: 'register-error-phone-min' }), - password: z - .string() - .min(8, { error: 'register-error-password-symbols' }) - .regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter' }) - .regex(/[0-9]/, { error: 'register-error-password-one-digit' }) - .max(124, { error: 'register-error-max-passowrd' }) - .trim(), - confirm_password: z - .string(), - agree: z - .string() - .min(2, { error: 'register-error-agree' }), - companyName: z - .string() - .optional() - .nullable() - .refine( - (value) => { - if (!value || value.trim().length === 0) return true; - return value.trim().length >= 2; - }, - { message: 'register-error-company-name-min' } - ) - .refine( - (value) => { - const trimmed = value ? value.trim() : ''; - return trimmed.length === 0 || trimmed.length <= 200; - }, - { - message: 'register-error-company-name-max', - path: ['companyName'] - } - ) - .refine( - (value) => { - if (!value || value.trim().length === 0) return true; - const regex = /^[a-zA-Zа-яА-ЯёЁ0-9\s\-&.,'"()]+$/; - return regex.test(value); - }, - { - message: 'companyName-invalid-chars', - path: ['companyName'] - } - ), - }) - .refine((data) => data.password === data.confirm_password, { - message: 'repeat-password', - path: ['confirm_password'], - }); export const loginFormSchema = z .object({ diff --git a/src/app/lib/validation-config-parser.ts b/src/app/lib/validation-config-parser.ts new file mode 100644 index 0000000..c07b792 --- /dev/null +++ b/src/app/lib/validation-config-parser.ts @@ -0,0 +1,225 @@ +import * as z from 'zod'; + +interface FieldConfig { + maxSize?: { value: number; message: string }; + minSize?: { value: number; message: string }; + allowedSymbols?: { value: string; message: string }; + optional?: { value: boolean }; + requiredDigit?: { value: boolean; message: string }; + requiredLetter?: { value: boolean; message: string }; + caseType?: { value: 'LOWER' | 'UPPER' | 'ANY'; message: string }; + emailValidation?: { value: boolean; message: string }; +} + +interface ValidationConfig { + [key: string]: FieldConfig; +} + +const EMAIL_REGEX = /^([a-z0-9.\-_]+|[а-яё0-9.\-_]+)@(([a-z0-9.\-]+)\.([a-z]{2,})|([a-z0-9.\-]+)\.([а-яё]{2,})|([а-яё0-9.\-]+)\.([a-z]{2,})|([а-яё0-9.\-]+)\.([а-яё]{2,}))$/i; + +export function createValidationSchema(config: ValidationConfig) { + const schema: Record = {}; + + // fullName + if (config.fullName) { + schema.fullName = z + .string() + .min(config.fullName.minSize?.value || 1, { + error: config.fullName.minSize?.message || 'register-error-name-min' + }) + .max(config.fullName.maxSize?.value || 100, { + error: config.fullName.maxSize?.message || 'register-error-name-max' + }) + .regex(new RegExp(config.fullName.allowedSymbols?.value || '.*'), { + message: config.fullName.allowedSymbols?.message || 'register-error-name-not-allowed-symbols' + }) + .trim(); + } + + // email + if (config.email) { + let emailSchema = z + .string() + .min(config.email.minSize?.value || 1, { + error: config.email.minSize?.message || 'register-error-email-min' + }) + .max(config.email.maxSize?.value || 254, { + error: config.email.maxSize?.message || 'register-error-email-max' + }) + .trim(); + + // Проверка на нижний регистр + if (config.email.caseType?.value === 'LOWER') { + emailSchema = emailSchema.refine( + (value) => value === value.toLowerCase(), + { message: config.email.caseType?.message || 'register-error-email-no-uppercase' } + ); + } + + // Проверка допустимых символов (опционально, если указано) + if (config.email.allowedSymbols?.value) { + emailSchema = emailSchema.regex( + new RegExp(config.email.allowedSymbols.value), + { message: config.email.allowedSymbols.message } + ); + } + + // Валидация email формата (если включено) + if (config.email.emailValidation?.value) { + emailSchema = emailSchema.refine( + (value) => EMAIL_REGEX.test(value), + { message: config.email.emailValidation.message || 'register-error-email-invalid' } + ); + } + + schema.email = emailSchema; + } + + // phone + if (config.phone) { + let phoneSchema = z.string(); + + // Минимальная длина + if (config.phone.minSize) { + phoneSchema = phoneSchema.min(config.phone.minSize.value, { + error: config.phone.minSize.message + }); + } + + // Максимальная длина + if (config.phone.maxSize) { + phoneSchema = phoneSchema.max(config.phone.maxSize.value, { + error: config.phone.maxSize.message + }); + } + + // Допустимые символы + if (config.phone.allowedSymbols) { + phoneSchema = phoneSchema.regex( + new RegExp(config.phone.allowedSymbols.value), + { message: config.phone.allowedSymbols.message } + ); + } + + schema.phone = phoneSchema; + } + + // password + if (config.password) { + let passwordSchema = z + .string() + .min(config.password.minSize?.value || 8, { + error: config.password.minSize?.message || 'register-error-password-symbols' + }) + .max(config.password.maxSize?.value || 124, { + error: config.password.maxSize?.message || 'register-error-max-password' + }) + .trim(); + + // Проверка допустимых символов + if (config.password.allowedSymbols?.value) { + passwordSchema = passwordSchema.regex( + new RegExp(config.password.allowedSymbols.value), + { message: config.password.allowedSymbols.message } + ); + } + + // Проверка наличия цифры + if (config.password.requiredDigit?.value) { + passwordSchema = passwordSchema.regex( + /[0-9]/, + { message: config.password.requiredDigit.message } + ); + } + + // Проверка наличия буквы + if (config.password.requiredLetter?.value) { + passwordSchema = passwordSchema.regex( + /[a-zA-Zа-яёА-ЯЁ]/, + { message: config.password.requiredLetter.message } + ); + } + + schema.password = passwordSchema; + } + + // companyName (опциональное поле) + if (config.companyName) { + let companySchema = z.string(); + + // Если поле опциональное, добавляем .optional().nullable() + if (config.companyName.optional?.value) { + companySchema = companySchema + .optional() + .nullable() + .transform((val) => (val === null || val === undefined ? '' : val)) + .refine( + (value) => { + // Для пустого значения пропускаем проверки + if (!value || value.trim().length === 0) return true; + + // Проверка минимальной длины + const minLength = config.companyName?.minSize?.value || 2; + return value.trim().length >= minLength; + }, + { + message: config.companyName.minSize?.message || 'register-error-company-name-min' + } + ) as unknown as z.ZodString; + } else { + // Если поле обязательное + if (config.companyName.minSize) { + companySchema = companySchema.min(config.companyName.minSize.value, { + error: config.companyName.minSize.message + }); + } + } + + // Максимальная длина (применяется всегда) + if (config.companyName.maxSize) { + companySchema = companySchema.refine( + (value) => { + if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) { + return true; + } + const maxLength = config.companyName?.maxSize?.value || 200; + return (value || '').trim().length <= maxLength; + }, + { + message: config.companyName.maxSize.message + } + ); + } + + // Допустимые символы + if (config.companyName.allowedSymbols?.value) { + companySchema = companySchema.refine( + (value) => { + if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) { + return true; + } + const regex = new RegExp(config.companyName.allowedSymbols!.value); + return regex.test(value || ''); + }, + { + message: config.companyName.allowedSymbols.message + } + ); + } + + companySchema = companySchema.transform((val) => val?.trim() || '') as unknown as z.ZodString; + + schema.companyName = companySchema; + } + + // Дополнительные поля для формы регистрации + schema.confirm_password = z.string(); + schema.agree = z.string().min(2, { error: 'register-error-agree' }); + + return z + .object(schema) + .refine((data) => data.password === data.confirm_password, { + message: 'repeat-password', + path: ['confirm_password'], + }); +} \ No newline at end of file diff --git a/validation-rules.json b/src/app/lib/validation-config.json similarity index 87% rename from validation-rules.json rename to src/app/lib/validation-config.json index 07f33ce..b4c9033 100644 --- a/validation-rules.json +++ b/src/app/lib/validation-config.json @@ -2,11 +2,11 @@ "fullName": { "maxSize": { "value": 100, - "message": "register-error-max-name" + "message": "register-error-name-max" }, "minSize": { "value": 2, - "message": "register-error-min-name" + "message": "register-error-name-min" }, "allowedSymbols": { "value": "^[a-zA-Zа-яА-ЯёЁ\\s\\-'.]*$", @@ -16,11 +16,11 @@ "email": { "maxSize": { "value": 254, - "message": "register-error-max-email" + "message": "register-error-email-max" }, "minSize": { "value": 7, - "message": "register-error-min-email" + "message": "register-error-email-min" }, "allowedSymbols": { "value": "^[a-zA-Zа-яА-ЯёЁ0-9@.!#$%&'*+/=?^_{|}~\\-]*$", @@ -38,11 +38,11 @@ "password": { "maxSize": { "value": 124, - "message": "register-error-max-name" + "message": "register-error-password-max" }, "minSize": { "value": 8, - "message": "register-error-min-name" + "message": "register-error-password-min" }, "allowedSymbols": { "value": "^[a-zA-Zа-яА-ЯёЁ0-9\\-'.]*$", diff --git a/src/app/ui/forms/register-form.tsx b/src/app/ui/forms/register-form.tsx index 9149426..efd2594 100644 --- a/src/app/ui/forms/register-form.tsx +++ b/src/app/ui/forms/register-form.tsx @@ -176,7 +176,16 @@ export default function RegisterForm() { {formState?.error?.phone && (

- {t(formState?.error?.phone)} + { + formState?.error?.phone.split('&').map((e, index) => { + return ( + + {t(e)} +
+
+ ) + }) + }

)} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 6e0b79a..4a04fd8 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -194,11 +194,11 @@ "privacy-policy": "privacy policy", "register": "Register", "register-error-agree": "Please review the terms of use and privacy policy.", - "register-error-min-name": "Name must be at least 2 characters long.", - "register-error-max-name": "The name length must not exceed 100 characters.", + "register-error-name-min": "Name must be at least 2 characters long.", + "register-error-name-max": "The name length must not exceed 100 characters.", "register-error-password-one-digit": "Password must contain at least 1 digit.", "register-error-password-one-letter": "Password must contain at least 1 letter.", - "register-error-password-symbols": "Password must be at least 8 characters long.", + "register-error-password-min": "Password must be at least 8 characters long.", "register-error-phone-min": "Please enter a phone number.", "register-error-company-name-min": "The company name must contain at least 2 characters.", "register-error-company-name-max": "The company name must exceed 200 characters.", @@ -220,10 +220,12 @@ "password-too-long": "Password must be less than 50 characters", "register-error-email-no-uppercase": "Email must not contain capital letters.", "no-emoji-allowed": "The use of emoji is prohibited.", - "register-error-max-passowrd": "The password length must not exceed 124 characters.", - "register-error-max-email": "Email length must not exceed 254 characters.", + "register-error-password-max": "The password length must not exceed 124 characters.", + "register-error-email-max": "Email length must not exceed 254 characters.", "companyName-invalid-chars": "The company name must not contain symbols.", "register-error-company-name-not-allowed-symbols": "The company name must not contain symbols.", - "password-too-common": "This password is too common, please choose another." + "password-too-common": "This password is too common, please choose another.", + "register-error-email-min": "Email length must be at least 7 characters.", + "register-error-phone-not-allowed-symbols": "" } } \ No newline at end of file diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 4b00775..79521ec 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -194,11 +194,11 @@ "privacy-policy": "политикой конфиденциальности", "register": "Зарегистрироваться", "register-error-agree": "Пожалуйста, ознакомьтесь с условиями использования и политикой конфиденциальности.", - "register-error-min-name": "Имя должно содержать не менее 2 символов.", - "register-error-max-name": "Длина имени не должна превышать 100 символов.", + "register-error-name-min": "Имя должно содержать не менее 2 символов.", + "register-error-name-max": "Длина имени не должна превышать 100 символов.", "register-error-password-one-digit": "Пароль должен содержать как минимум 1 цифру.", "register-error-password-one-letter": "Пароль должен содержать как минимум 1 букву.", - "register-error-password-symbols": "Длина пароля должна быть не менее 8 символов.", + "register-error-password-min": "Длина пароля должна быть не менее 8 символов.", "register-error-phone-min": "Пожалуйста, введите телефонный номер.", "register-error-company-name-min": "Название компании должно содержать не менее 2 символов.", "register-error-company-name-max": "Название компании должно превышать 200 символов.", @@ -220,10 +220,12 @@ "password-too-long": "Пароль должен содержать менее 50 символов.", "register-error-email-no-uppercase": "Email не должен содержать заглавных букв.", "no-emoji-allowed": "Использование эмодзи запрещено.", - "register-error-max-passowrd": "Длина пароля не должна превышать 124 символа.", - "register-error-max-email": "Длина Email не должна превышать 254 символа.", + "register-error-password-max": "Длина пароля не должна превышать 124 символа.", + "register-error-email-max": "Длина Email не должна превышать 254 символа.", "companyName-invalid-chars": "Название компании не должно содержать символов.", "register-error-company-name-not-allowed-symbols": "Название компании не должно содержать символов.", - "password-too-common": "Этот пароль слишком простой, пожалуйста, выберите другой." + "password-too-common": "Этот пароль слишком простой, пожалуйста, выберите другой.", + "register-error-email-min": "Длина Email не должна быть меньше 7 символов.", + "register-error-phone-not-allowed-symbols": "" } } \ No newline at end of file