add reset password

This commit is contained in:
smanylov
2026-02-02 15:00:56 +07:00
parent 19e807a7f6
commit 7789f19055
8 changed files with 457 additions and 95 deletions
@@ -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 (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
-4
View File
@@ -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 (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
+34
View File
@@ -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 (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
<h1>NO COPY</h1>
<p>
{t('recover-password')}
</p>
</div>
<ResetPasswordForm />
<div className={`${styles['register-link']}`}>
<p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
</div>
<div className={`${styles['register-link']}`}>
<p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
</p>
</div>
</div>
</div>
)
}
+148
View File
@@ -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<string, string> | null
};
export async function confirmPassword(
state: FormStateConfirmPassword | undefined,
formData: any,
): Promise<FormStateConfirmPassword | undefined> {
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<string, string> = {}
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<string, string> = {};
if (error) {
return {
previousState: {
email,
password,
confirm_password,
verifyToken
},
error: errors
};
}
throw error;
}
redirect('/pages/dashboard');
}
+9 -5
View File
@@ -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<any> | null = null;
async function loadConfigFromBackend(accountType: 'b2b' | 'b2c'): Promise<any> {
async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise<any> {
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<any> {
}
}
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;
})();
+53 -69
View File
@@ -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<string, any> = {};
// 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, {
+5 -13
View File
@@ -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' && (
<p className={`${styles['form-error']}`}>
{t(state?.error?.email)}
Восстановление пароля не работает
</p>
)} */}
</div>
@@ -38,15 +37,8 @@ export default function ForgotPasswordForm() {
{t(state?.error.server)}
</p>
)} */}
<div className={`${styles['form-options']}`}>
<div className={`${styles['form-checkbox']}`}>
<small>
функционал не реализован
</small>
</div>
</div>
<button type="submit" className={`${styles['btn']}`}>
{t('sign-in')}
Восстановить пароль
</button>
</form>
)
+208
View File
@@ -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<HTMLInputElement>(null);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [formState, setFormState] = useState<FormStateConfirmPassword | undefined>(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<HTMLInputElement>): 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 (
<form action={formAction}>
<div className={`${styles['form-group']}`}>
<input
type="hidden"
id="email"
name="email"
className={`${styles['form-input']}`}
placeholder={t('enter-email')}
defaultValue={email ? email : ''}
/>
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>
Код восстановления
</label>
<input
type="readOnly"
id="verify-token"
name="verify-token"
className={`${styles['form-input']}`}
placeholder={'Код восстановления'}
defaultValue={formState?.previousState?.verifyToken ? formState?.previousState?.verifyToken : ''}
/>
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('password')}
</label>
<div
className={`${styles['password-wrapper']}`}
>
<input
ref={passwordRef}
type={showPassword ? "text" : "password"}
id="password"
name="password"
className={`${styles['form-input']} ${styles['password']}`}
placeholder={t('password-placeholder')}
defaultValue={formState?.previousState?.password ? formState?.previousState?.password : ''}
onChange={onChangeHandler}
/>
<button
onClick={() => {
showPassowrd('password')
}}
type="button"
className={`show-password-button ${showPassword ? 'show' : ''}`}
>
<IconEye />
</button>
</div>
{formState?.error?.password && (
<p className="text-sm text-red-500">
{
formState?.error?.password.split('&').map((e, index) => {
return (
<span key={index}>
{t(e)}
<br />
</span>
)
})
}
</p>
)}
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('confirm-password')}
</label>
<div className={`${styles['password-wrapper']}`}>
<input
type={showConfirmPassword ? "text" : "password"}
id="confirm_password"
name="confirm_password"
className={`${styles['form-input']} ${styles['password']}`}
placeholder={t('repeat-password')}
defaultValue={formState?.previousState?.confirm_password ? formState?.previousState?.confirm_password : ''}
onChange={onChangeHandler}
/>
<button
onClick={() => {
showPassowrd('confirm-password');
}}
type="button"
className={`show-password-button ${showConfirmPassword ? 'show' : ''}`}
>
<IconEye />
</button>
</div>
{formState?.error?.confirm_password && (
<p className="text-sm text-red-500">
{t(formState?.error?.confirm_password)}
</p>
)}
</div>
{/* {state?.error.server && (
<p className={`${styles['form-error']}`}>
{t(state?.error.server)}
</p>
)} */}
<button type="submit" className={`${styles['btn']}`}>
Изменить пароль
</button>
</form>
)
}