auth validation
This commit is contained in:
@@ -104,7 +104,8 @@ export const Checkbox: React.FC<CheckboxProps> = ({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'group-hover:bg-default-100 group-active:scale-90 flex items-center justify-center bg-transparent hover:bg-default-100 box-border border-solid border-[1px] border-liquid-white z-10 relative transition-all duration-300',
|
||||
'group-hover:bg-default-100 group-active:scale-90 flex items-center justify-center bg-transparent hover:bg-default-100 box-border border-solid border-[1px] border-liquid-white z-10 relative transition-all duration-300',
|
||||
color == 'danger' && ' border-liquid-red',
|
||||
sizeVariants[size],
|
||||
radiusVraiants[radius],
|
||||
active && borderColorsVariants[color],
|
||||
|
||||
@@ -84,7 +84,7 @@ export const Input: React.FC<inputProps> = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'text-liquid-red text-[14px] h-[18px] text-right mt-[5px]',
|
||||
'text-liquid-red text-[14px] h-auto text-right mt-[5px] whitespace-pre-line ',
|
||||
error == '' && 'h-0 mt-0',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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<string, string[]>;
|
||||
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<string, string[]>;
|
||||
});
|
||||
|
||||
// ----------------- Login -----------------
|
||||
@@ -304,5 +320,5 @@ const authSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { logout } = authSlice.actions;
|
||||
export const { logout, setAuthStatus } = authSlice.actions;
|
||||
export const authReducer = authSlice.reducer;
|
||||
|
||||
@@ -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 (
|
||||
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
|
||||
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
|
||||
@@ -73,6 +87,7 @@ const Login = () => {
|
||||
setUsername(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
error={getErrorLoginMessage()}
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
@@ -84,6 +99,7 @@ const Login = () => {
|
||||
setPassword(v);
|
||||
}}
|
||||
placeholder="abCD1234"
|
||||
error={getErrorPasswordMessage()}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end mt-[10px]">
|
||||
|
||||
@@ -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<string>('');
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||
const [politicChecked, setPoliticChecked] = useState<boolean>(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 (
|
||||
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center">
|
||||
<div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
|
||||
@@ -76,6 +162,7 @@ const Register = () => {
|
||||
setEmail(v);
|
||||
}}
|
||||
placeholder="example@gmail.com"
|
||||
error={getErrorEmailMessage()}
|
||||
/>
|
||||
<Input
|
||||
name="login"
|
||||
@@ -87,6 +174,7 @@ const Register = () => {
|
||||
setUsername(v);
|
||||
}}
|
||||
placeholder="login"
|
||||
error={getErrorLoginMessage()}
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
@@ -98,6 +186,7 @@ const Register = () => {
|
||||
setPassword(v);
|
||||
}}
|
||||
placeholder="abCD1234"
|
||||
error={getErrorPasswordMessage()}
|
||||
/>
|
||||
<Input
|
||||
name="confirm-password"
|
||||
@@ -109,23 +198,31 @@ const Register = () => {
|
||||
setConfirmPassword(v);
|
||||
}}
|
||||
placeholder="abCD1234"
|
||||
error={getErrorConfirmPasswordMessage()}
|
||||
/>
|
||||
|
||||
<div className=" flex items-center mt-[10px] h-[24px]">
|
||||
<Checkbox
|
||||
onChange={(value: boolean) => {
|
||||
value;
|
||||
setPoliticChecked(value);
|
||||
}}
|
||||
className="p-0 w-fit m-[2.75px]"
|
||||
size="md"
|
||||
color="secondary"
|
||||
color={
|
||||
politicChecked || !submitClicked
|
||||
? 'secondary'
|
||||
: 'danger'
|
||||
}
|
||||
variant="default"
|
||||
/>
|
||||
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]">
|
||||
Я принимаю{' '}
|
||||
<Link to={'/home'} className={' underline'}>
|
||||
{/* <Link to={'/home'} className={' underline'}>
|
||||
политику конфиденциальности
|
||||
</Link>
|
||||
</Link> */}
|
||||
<span className={' underline cursor-pointer'}>
|
||||
политику конфиденциальности
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user