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
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],

View File

@@ -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',
)}
>

View File

@@ -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;

View File

@@ -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]">

View File

@@ -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>