diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx index 180363e..5877c4b 100644 --- a/src/components/checkbox/Checkbox.tsx +++ b/src/components/checkbox/Checkbox.tsx @@ -104,7 +104,8 @@ export const Checkbox: React.FC = ({ >
= ({
diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts index eeabf20..0aebac2 100644 --- a/src/redux/slices/auth.ts +++ b/src/redux/slices/auth.ts @@ -1,6 +1,8 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import axios from '../../axios'; +type Status = 'idle' | 'loading' | 'successful' | 'failed'; + // 🔹 Декодирование JWT function decodeJwt(token: string) { const [, payload] = token.split('.'); @@ -15,8 +17,12 @@ interface AuthState { username: string | null; email: string | null; id: string | null; - status: 'idle' | 'loading' | 'successful' | 'failed'; + status: Status; error: string | null; + register: { + errors?: Record; + status: Status; + }; } // 🔹 Инициализация состояния с синхронной загрузкой из localStorage @@ -31,6 +37,9 @@ const initialState: AuthState = { id: null, status: 'idle', error: null, + register: { + status: 'idle', + }, }; // Если токен есть, подставляем в axios и декодируем @@ -76,9 +85,7 @@ export const registerUser = createAsyncThunk( }); return response.data; } catch (err: any) { - return rejectWithValue( - err.response?.data?.message || 'Registration failed', - ); + return rejectWithValue(err.response?.data?.errors); } }, ); @@ -165,6 +172,15 @@ const authSlice = createSlice({ localStorage.removeItem('refreshToken'); delete axios.defaults.headers.common['Authorization']; }, + setAuthStatus: ( + state, + action: PayloadAction<{ key: keyof AuthState; status: Status }>, + ) => { + const { key, status } = action.payload; + if (state[key]) { + (state[key] as any).status = status; + } + }, }, extraReducers: (builder) => { // ----------------- Register ----------------- @@ -199,7 +215,7 @@ const authSlice = createSlice({ }); builder.addCase(registerUser.rejected, (state, action) => { state.status = 'failed'; - state.error = action.payload as string; + state.register.errors = action.payload as Record; }); // ----------------- Login ----------------- @@ -304,5 +320,5 @@ const authSlice = createSlice({ }, }); -export const { logout } = authSlice.actions; +export const { logout, setAuthStatus } = authSlice.actions; export const authReducer = authSlice.reducer; diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index a660b35..90fabe1 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -27,7 +27,6 @@ const Login = () => { // После успешного логина useEffect(() => { dispatch(setMenuActivePage('account')); - submitClicked; }, []); useEffect(() => { @@ -47,6 +46,21 @@ const Login = () => { dispatch(loginUser({ username, password })); }; + const getErrorLoginMessage = (): string => { + if (!submitClicked) return ''; + if (username == '') return 'Поле не может быть пустым'; + if (password == '') return ''; + if (status === 'failed') + return 'Неверное имя пользователя и/или пароль'; + return ''; + }; + + const getErrorPasswordMessage = (): string => { + if (!submitClicked) return ''; + if (password == '') return 'Поле не может быть пустым'; + return ''; + }; + return (
@@ -73,6 +87,7 @@ const Login = () => { setUsername(v); }} placeholder="login" + error={getErrorLoginMessage()} /> { setPassword(v); }} placeholder="abCD1234" + error={getErrorPasswordMessage()} />
diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index 4eee15a..58ef390 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -13,6 +13,37 @@ import { SecondaryButton } from '../../../components/button/SecondaryButton'; import { Checkbox } from '../../../components/checkbox/Checkbox'; import { googleLogo } from '../../../assets/icons/input'; +function isValidEmail(email: string): boolean { + const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return pattern.test(email); +} + +function isValidLogin(login: string): boolean { + return login.length >= 4 && login.length <= 128; +} + +function isValidatePassword(password: string): string { + const errors: string[] = []; + + if (password.length < 8 || password.length > 255) { + errors.push('Пароль должен содержать от 8 до 255 символов'); + } + + if (!/[A-Z]/.test(password)) { + errors.push('Пароль должен содержать хотя бы одну заглавную букву'); + } + + if (!/[a-z]/.test(password)) { + errors.push('Пароль должен содержать хотя бы одну строчную букву'); + } + + if (!/[0-9]/.test(password)) { + errors.push('Пароль должен содержать хотя бы одну цифру'); + } + + return errors.join('\n'); +} + const Register = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -23,10 +54,10 @@ const Register = () => { const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [submitClicked, setSubmitClicked] = useState(false); + const [politicChecked, setPoliticChecked] = useState(false); const { status, jwt } = useAppSelector((state) => state.auth); - - // После успешной регистрации — переход в систему + // const { errors } = useAppSelector((state) => state.auth.register); useEffect(() => { dispatch(setMenuActivePage('account')); @@ -38,18 +69,73 @@ const Register = () => { const path = from ? from.pathname + from.search : '/home/account'; navigate(path, { replace: true }); } - submitClicked; }, [jwt]); const handleRegister = () => { setSubmitClicked(true); + if (!politicChecked) return; if (!username || !email || !password || !confirmPassword) return; if (password !== confirmPassword) return; + if ( + !isValidEmail(email) || + !isValidLogin(username) || + isValidatePassword(password) != '' + ) + return; dispatch(registerUser({ username, email, password })); }; + const getErrorEmailMessage = (): string => { + if (!submitClicked) return ''; + if (email == '') return 'Поле не может быть пустым'; + if (!isValidEmail(email)) return 'Почта не валидна'; + if (!username || !email || !password || !confirmPassword) return ''; + if (password !== confirmPassword) return ''; + // if (errors?.Email) { + // return errors.Email.join('\n'); + // } + return ''; + }; + + const getErrorLoginMessage = (): string => { + if (!submitClicked) return ''; + if (username == '') return 'Поле не может быть пустым'; + if (!isValidLogin(username)) + return 'Логин дложен быть длиной от 4 до 128 символов'; + if (!username || !email || !password || !confirmPassword) return ''; + if (password !== confirmPassword) return ''; + // if (errors?.Username) { + // return errors.Username.join('\n'); + // } + return ''; + }; + + const getErrorPasswordMessage = (): string => { + if (!submitClicked) return ''; + if (password == '') return 'Поле не может быть пустым'; + const val = isValidatePassword(password); + if (val != '') return val; + if (confirmPassword != password) return 'Пароли не совпадают'; + if (!username || !email || !password || !confirmPassword) return ''; + // if (errors?.Password) { + // return errors.Password.join('\n'); + // } + return ''; + }; + + const getErrorConfirmPasswordMessage = (): string => { + if (!submitClicked) return ''; + if (confirmPassword == '') return 'Поле не может быть пустым'; + const val = isValidatePassword(confirmPassword); + if (val != '') return val; + if (confirmPassword != password) return 'Пароли не совпадают'; + if (!username || !email || !password || !confirmPassword) return ''; + + return ''; + }; + return (
@@ -76,6 +162,7 @@ const Register = () => { setEmail(v); }} placeholder="example@gmail.com" + error={getErrorEmailMessage()} /> { setUsername(v); }} placeholder="login" + error={getErrorLoginMessage()} /> { setPassword(v); }} placeholder="abCD1234" + error={getErrorPasswordMessage()} /> { setConfirmPassword(v); }} placeholder="abCD1234" + error={getErrorConfirmPasswordMessage()} />
{ - value; + setPoliticChecked(value); }} className="p-0 w-fit m-[2.75px]" size="md" - color="secondary" + color={ + politicChecked || !submitClicked + ? 'secondary' + : 'danger' + } variant="default" /> Я принимаю{' '} - + {/* политику конфиденциальности - + */} + + политику конфиденциальности +