diff --git a/src/app/[locale]/forgot-password/page.tsx b/src/app/[locale]/forgot-password/page.tsx index 1aff5a0..34d5374 100644 --- a/src/app/[locale]/forgot-password/page.tsx +++ b/src/app/[locale]/forgot-password/page.tsx @@ -2,7 +2,6 @@ import { useTranslations } from 'next-intl' import LogoIcon from '@/app/ui/logo-icon'; import styles from '@/app/styles/module/login.module.scss'; import Link from 'next/link'; -import LanguageSwitcher from '@/app/components/LanguageSwitcher'; import ForgotPasswordForm from '@/app/ui/forms/forgot-password-form'; export default function Page() { @@ -10,9 +9,6 @@ export default function Page() { return (
-
- -
diff --git a/src/app/[locale]/register/page.tsx b/src/app/[locale]/register/page.tsx index 7293a9e..0b4eb3b 100644 --- a/src/app/[locale]/register/page.tsx +++ b/src/app/[locale]/register/page.tsx @@ -2,7 +2,6 @@ import LogoIcon from '@/app/ui/logo-icon'; import styles from '@/app/styles/module/login.module.scss' import Link from 'next/link'; import RegisterForm from '@/app/ui/forms/register-form'; -import LanguageSwitcher from '@/app/components/LanguageSwitcher'; import {useTranslations} from 'next-intl'; export default function Page() { @@ -10,9 +9,6 @@ export default function Page() { return (
-
- -
diff --git a/src/app/[locale]/reset-password/page.tsx b/src/app/[locale]/reset-password/page.tsx new file mode 100644 index 0000000..eac8849 --- /dev/null +++ b/src/app/[locale]/reset-password/page.tsx @@ -0,0 +1,34 @@ +import { useTranslations } from 'next-intl' +import LogoIcon from '@/app/ui/logo-icon'; +import styles from '@/app/styles/module/login.module.scss'; +import Link from 'next/link'; +import ResetPasswordForm from '@/app/ui/forms/reset-password-form'; + +export default function Page() { + const t = useTranslations('Login-register-form'); + + return ( +
+
+
+ +

NO COPY

+

+ {t('recover-password')} +

+
+ + + +
+

{t('already-have-an-account')}? {t('login')}

+
+
+

{t('no-account')}? + {t('register')} +

+
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index 5f7b474..39cfbcd 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -341,4 +341,152 @@ export async function confirmEmail( console.log('confirmEmail'); const emailConfirm = formData.get('emailConfirm') as string || ''; console.log(emailConfirm); +} + +export async function resetPassword( + state: any | undefined, + formData: any, +) { + const email = formData.get('email'); + console.log('resetPassword'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + version: 1, + msg_id: 20010, + message_body: { + email: email, + action: "resetPassword" + } + }), + }); + + if (response.ok) { + + const parsed = await response.json(); + console.log(parsed); + if (parsed.message_code === 0) { + + } else { + throw parsed.message_body; + } + } + } catch (error) { + throw error; + } + + redirect(`/reset-password?email=${email}`); +} + +export type FormStateConfirmPassword = { + previousState: { + email: string; + password: string; + confirm_password: string; + verifyToken: string; + }, + error: Record | null +}; + +export async function confirmPassword( + state: FormStateConfirmPassword | undefined, + formData: any, +): Promise { + const email = formData.get('email'); + const password = formData.get('password'); + const confirm_password = formData.get('confirm_password'); + const verifyToken = formData.get('verify-token'); + console.log('confirmPassword'); + + const SignupFormSchema = await getSignupFormSchema('resetPassword'); + + const dataToValidate = { + password, + confirm_password + }; + + const validatedFields = SignupFormSchema.safeParse(dataToValidate); + + console.log(validatedFields); + + if (!validatedFields.success) { + const errors: Record = {} + JSON.parse(validatedFields.error.message).forEach((obj: { + message: string, + path: string + }) => { + if (errors[obj.path[0]]) { + errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`; + } else { + errors[obj.path[0]] = obj.message; + } + }); + + return { + previousState: { + email, + password, + confirm_password, + verifyToken + }, + error: errors + }; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + version: 1, + msg_id: 20010, + message_body: { + email: email, + password: password, + verifyToken: verifyToken, + action: "confirmVerification" + } + }), + }); + + if (response.ok) { + + const parsed = await response.json(); + console.log(parsed); + if (parsed.message_code === 0) { + await createSession(parsed.message_body.authToken, email as string); + } else { + throw parsed.message_body; + } + } else { + throw (`${response.status}`); + } + } catch (error: unknown) { + const typedError = error as any; + const errors: Record = {}; + + if (error) { + return { + previousState: { + email, + password, + confirm_password, + verifyToken + }, + error: errors + }; + } + throw error; + } + + redirect('/pages/dashboard'); } \ No newline at end of file diff --git a/src/app/actions/definitions.ts b/src/app/actions/definitions.ts index 6488098..51e9662 100644 --- a/src/app/actions/definitions.ts +++ b/src/app/actions/definitions.ts @@ -1,5 +1,5 @@ import * as z from 'zod' -import { createValidationSchema } from '@/app/lib/validation-config-parser'; +import { createValidationSchema, createValidationSchemaForPssword } from '@/app/lib/validation-config-parser'; import validationConfig from '@/app/lib/validation-config.json'; export type FormState = @@ -38,7 +38,7 @@ const defaultConfig = validationConfig as ValidationConfig; let cachedSignupSchema: any = null; let schemaPromise: Promise | null = null; -async function loadConfigFromBackend(accountType: 'b2b' | 'b2c'): Promise { +async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise { try { const response = await fetch('/api/validation-config'); if (!response.ok) throw new Error('Failed to load config'); @@ -50,15 +50,19 @@ async function loadConfigFromBackend(accountType: 'b2b' | 'b2c'): Promise { } } -export async function getSignupFormSchema(accountType: 'b2b' | 'b2c') { +export async function getSignupFormSchema(schemaType?: 'b2b' | 'b2c' | 'resetPassword') { if (cachedSignupSchema) { return cachedSignupSchema; } if (!schemaPromise) { schemaPromise = (async () => { - const config = await loadConfigFromBackend(accountType); - cachedSignupSchema = createValidationSchema(config, accountType); + const config = await loadConfigFromBackend(); + if (schemaType === 'resetPassword') { + cachedSignupSchema = createValidationSchemaForPssword(config); + } else { + cachedSignupSchema = createValidationSchema(config, schemaType); + } return cachedSignupSchema; })(); diff --git a/src/app/lib/validation-config-parser.ts b/src/app/lib/validation-config-parser.ts index e6bcf97..d3a3825 100644 --- a/src/app/lib/validation-config-parser.ts +++ b/src/app/lib/validation-config-parser.ts @@ -163,79 +163,63 @@ export function createValidationSchema(config: ValidationConfig, accountType?: ' 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'], + }); +} + +export function createValidationSchemaForPssword(config: ValidationConfig) { + const schema: Record = {}; + + // 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; + } + + // Дополнительные поля для формы регистрации + schema.confirm_password = z.string(); + return z .object(schema) .refine((data) => data.password === data.confirm_password, { diff --git a/src/app/ui/forms/forgot-password-form.tsx b/src/app/ui/forms/forgot-password-form.tsx index 6c71e9b..93f48c5 100644 --- a/src/app/ui/forms/forgot-password-form.tsx +++ b/src/app/ui/forms/forgot-password-form.tsx @@ -4,12 +4,11 @@ import styles from '@/app/styles/module/login.module.scss' import { useActionState } from 'react'; import { useTranslations } from 'next-intl'; +import {resetPassword} from '@/app/actions/auth'; export default function ForgotPasswordForm() { const [state, formAction, isPending] = useActionState( - () => { - console.log("test"); - }, + resetPassword, undefined, ); const t = useTranslations('Login-register-form'); @@ -27,9 +26,9 @@ export default function ForgotPasswordForm() { className={`${styles['form-input']}`} placeholder={t('enter-email')} /> - {/* {state?.error?.email && ( + {/* {state === 'error' && (

- {t(state?.error?.email)} + Восстановление пароля не работает

)} */}
@@ -38,15 +37,8 @@ export default function ForgotPasswordForm() { {t(state?.error.server)}

)} */} -
-
- - функционал не реализован - -
-
) diff --git a/src/app/ui/forms/reset-password-form.tsx b/src/app/ui/forms/reset-password-form.tsx new file mode 100644 index 0000000..3550d9c --- /dev/null +++ b/src/app/ui/forms/reset-password-form.tsx @@ -0,0 +1,208 @@ +'use client' + +import styles from '@/app/styles/module/login.module.scss' +import { useActionState, useEffect, useRef, useState, ChangeEvent } from 'react'; + +import { useTranslations } from 'next-intl'; +import { confirmPassword, FormStateConfirmPassword } from '@/app/actions/auth'; +import { useSearchParams } from 'next/navigation'; +import { IconEye } from '@/app/ui/icons/icons'; +import { SignupFormSchema } from '@/app/actions/definitions'; +import * as z from 'zod'; + +export default function ResetPasswordForm() { + const [state, formAction, isPending] = useActionState( + confirmPassword, + undefined, + ); + const passwordRef = useRef(null); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [formState, setFormState] = useState(undefined); + const t = useTranslations('Login-register-form'); + const searchParams = useSearchParams(); + const email = searchParams.get('email'); + console.log(email); + + useEffect(() => { + if (state) { + setFormState(state); + } + }, [state]) + + function showPassowrd(target: 'password' | 'confirm-password') { + if (target === 'password') { + setShowPassword(!showPassword); + } else { + setShowConfirmPassword(!showConfirmPassword); + } + } + + function validateField( + fieldName: string, + value: string, + ) { + switch (fieldName) { + case 'confirm_password': + const baseSchema = SignupFormSchema.shape.confirm_password; + const baseResult = baseSchema.safeParse(value); + + if (!baseResult.success) { + return baseResult; + } + + const currentPassword = passwordRef.current?.value || ''; + if (value !== currentPassword) { + return { + success: false, + error: new z.ZodError([ + { + code: z.ZodIssueCode.custom, + message: 'repeat-password', + path: ['confirm_password'] + } + ]) + }; + } + + return { success: true, data: value }; + case 'password': + const schema = SignupFormSchema.shape[fieldName]; + return schema.safeParse(value); + + default: + throw new Error(`Unknown field: ${fieldName}`); + } + } + + function onChangeHandler(e: ChangeEvent): void { + const fieldName = e.target.name; + if (fieldName !== 'phone') { + e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, ''); + } + const validatedField = validateField(fieldName, e.target.value); + + if (validatedField.success) { + setFormState(prevState => { + if (!prevState?.error || !prevState?.error[fieldName]) { + return prevState; + } + + const { [fieldName]: _, ...newError } = prevState.error; + + return { + ...prevState, + error: newError + }; + }); + } + } + + return ( +
+
+ +
+
+ + +
+
+ +
+ + +
+ {formState?.error?.password && ( +

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

+ )} +
+
+ +
+ + +
+ {formState?.error?.confirm_password && ( +

+ {t(formState?.error?.confirm_password)} +

+ )} +
+ {/* {state?.error.server && ( +

+ {t(state?.error.server)} +

+ )} */} + +
+ ) +} \ No newline at end of file