auth validation
This commit is contained in:
@@ -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],
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user