auth validation

This commit is contained in:
Виталий Лавшонок
2025-11-23 13:53:48 +03:00
parent dae4584840
commit ee0e44082a
5 changed files with 147 additions and 17 deletions

View File

@@ -104,7 +104,8 @@ export const Checkbox: React.FC<CheckboxProps> = ({
> >
<div <div
className={cn( 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], sizeVariants[size],
radiusVraiants[radius], radiusVraiants[radius],
active && borderColorsVariants[color], active && borderColorsVariants[color],

View File

@@ -84,7 +84,7 @@ export const Input: React.FC<inputProps> = ({
<div <div
className={cn( 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', error == '' && 'h-0 mt-0',
)} )}
> >

View File

@@ -1,6 +1,8 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios'; import axios from '../../axios';
type Status = 'idle' | 'loading' | 'successful' | 'failed';
// 🔹 Декодирование JWT // 🔹 Декодирование JWT
function decodeJwt(token: string) { function decodeJwt(token: string) {
const [, payload] = token.split('.'); const [, payload] = token.split('.');
@@ -15,8 +17,12 @@ interface AuthState {
username: string | null; username: string | null;
email: string | null; email: string | null;
id: string | null; id: string | null;
status: 'idle' | 'loading' | 'successful' | 'failed'; status: Status;
error: string | null; error: string | null;
register: {
errors?: Record<string, string[]>;
status: Status;
};
} }
// 🔹 Инициализация состояния с синхронной загрузкой из localStorage // 🔹 Инициализация состояния с синхронной загрузкой из localStorage
@@ -31,6 +37,9 @@ const initialState: AuthState = {
id: null, id: null,
status: 'idle', status: 'idle',
error: null, error: null,
register: {
status: 'idle',
},
}; };
// Если токен есть, подставляем в axios и декодируем // Если токен есть, подставляем в axios и декодируем
@@ -76,9 +85,7 @@ export const registerUser = createAsyncThunk(
}); });
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
return rejectWithValue( return rejectWithValue(err.response?.data?.errors);
err.response?.data?.message || 'Registration failed',
);
} }
}, },
); );
@@ -165,6 +172,15 @@ const authSlice = createSlice({
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
delete axios.defaults.headers.common['Authorization']; 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) => { extraReducers: (builder) => {
// ----------------- Register ----------------- // ----------------- Register -----------------
@@ -199,7 +215,7 @@ const authSlice = createSlice({
}); });
builder.addCase(registerUser.rejected, (state, action) => { builder.addCase(registerUser.rejected, (state, action) => {
state.status = 'failed'; state.status = 'failed';
state.error = action.payload as string; state.register.errors = action.payload as Record<string, string[]>;
}); });
// ----------------- Login ----------------- // ----------------- Login -----------------
@@ -304,5 +320,5 @@ const authSlice = createSlice({
}, },
}); });
export const { logout } = authSlice.actions; export const { logout, setAuthStatus } = authSlice.actions;
export const authReducer = authSlice.reducer; export const authReducer = authSlice.reducer;

View File

@@ -27,7 +27,6 @@ const Login = () => {
// После успешного логина // После успешного логина
useEffect(() => { useEffect(() => {
dispatch(setMenuActivePage('account')); dispatch(setMenuActivePage('account'));
submitClicked;
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -47,6 +46,21 @@ const Login = () => {
dispatch(loginUser({ username, password })); 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 ( return (
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center"> <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 "> <div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
@@ -73,6 +87,7 @@ const Login = () => {
setUsername(v); setUsername(v);
}} }}
placeholder="login" placeholder="login"
error={getErrorLoginMessage()}
/> />
<Input <Input
name="password" name="password"
@@ -84,6 +99,7 @@ const Login = () => {
setPassword(v); setPassword(v);
}} }}
placeholder="abCD1234" placeholder="abCD1234"
error={getErrorPasswordMessage()}
/> />
<div className="flex justify-end mt-[10px]"> <div className="flex justify-end mt-[10px]">

View File

@@ -13,6 +13,37 @@ import { SecondaryButton } from '../../../components/button/SecondaryButton';
import { Checkbox } from '../../../components/checkbox/Checkbox'; import { Checkbox } from '../../../components/checkbox/Checkbox';
import { googleLogo } from '../../../assets/icons/input'; 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 Register = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -23,10 +54,10 @@ const Register = () => {
const [password, setPassword] = useState<string>(''); const [password, setPassword] = useState<string>('');
const [confirmPassword, setConfirmPassword] = useState<string>(''); const [confirmPassword, setConfirmPassword] = useState<string>('');
const [submitClicked, setSubmitClicked] = useState<boolean>(false); const [submitClicked, setSubmitClicked] = useState<boolean>(false);
const [politicChecked, setPoliticChecked] = useState<boolean>(false);
const { status, jwt } = useAppSelector((state) => state.auth); const { status, jwt } = useAppSelector((state) => state.auth);
// const { errors } = useAppSelector((state) => state.auth.register);
// После успешной регистрации — переход в систему
useEffect(() => { useEffect(() => {
dispatch(setMenuActivePage('account')); dispatch(setMenuActivePage('account'));
@@ -38,18 +69,73 @@ const Register = () => {
const path = from ? from.pathname + from.search : '/home/account'; const path = from ? from.pathname + from.search : '/home/account';
navigate(path, { replace: true }); navigate(path, { replace: true });
} }
submitClicked;
}, [jwt]); }, [jwt]);
const handleRegister = () => { const handleRegister = () => {
setSubmitClicked(true); setSubmitClicked(true);
if (!politicChecked) return;
if (!username || !email || !password || !confirmPassword) return; if (!username || !email || !password || !confirmPassword) return;
if (password !== confirmPassword) return; if (password !== confirmPassword) return;
if (
!isValidEmail(email) ||
!isValidLogin(username) ||
isValidatePassword(password) != ''
)
return;
dispatch(registerUser({ username, email, password })); 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 ( return (
<div className="h-svh w-svw fixed pointer-events-none top-0 left-0 flex items-center justify-center"> <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 "> <div className="grid gap-[80px] grid-cols-[400px,384px] box-border relative ">
@@ -76,6 +162,7 @@ const Register = () => {
setEmail(v); setEmail(v);
}} }}
placeholder="example@gmail.com" placeholder="example@gmail.com"
error={getErrorEmailMessage()}
/> />
<Input <Input
name="login" name="login"
@@ -87,6 +174,7 @@ const Register = () => {
setUsername(v); setUsername(v);
}} }}
placeholder="login" placeholder="login"
error={getErrorLoginMessage()}
/> />
<Input <Input
name="password" name="password"
@@ -98,6 +186,7 @@ const Register = () => {
setPassword(v); setPassword(v);
}} }}
placeholder="abCD1234" placeholder="abCD1234"
error={getErrorPasswordMessage()}
/> />
<Input <Input
name="confirm-password" name="confirm-password"
@@ -109,23 +198,31 @@ const Register = () => {
setConfirmPassword(v); setConfirmPassword(v);
}} }}
placeholder="abCD1234" placeholder="abCD1234"
error={getErrorConfirmPasswordMessage()}
/> />
<div className=" flex items-center mt-[10px] h-[24px]"> <div className=" flex items-center mt-[10px] h-[24px]">
<Checkbox <Checkbox
onChange={(value: boolean) => { onChange={(value: boolean) => {
value; setPoliticChecked(value);
}} }}
className="p-0 w-fit m-[2.75px]" className="p-0 w-fit m-[2.75px]"
size="md" size="md"
color="secondary" color={
politicChecked || !submitClicked
? 'secondary'
: 'danger'
}
variant="default" variant="default"
/> />
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]"> <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> </span>
</div> </div>