From afe0b8880878e67a311ac1265d0d30892a179da3 Mon Sep 17 00:00:00 2001 From: TBS093A Date: Thu, 6 Mar 2025 14:30:33 +0100 Subject: [PATCH] feat(forms & pages & services): add a lot of stuff improve login & register (integrate that with redux and api) + improve user settings dashboard (divide forms to particular files as form components) + improve formGenerator (catching refs and etc) + more --- package-lock.json | 15 ++ package.json | 1 + src/components/forms/formGenerator.js | 59 ++--- src/components/forms/user_auth/userLogin.js | 145 +++++----- .../forms/user_auth/userRegister.js | 193 +++++++------- .../user_settings/userChangePasswordForm.js | 137 ++++++++++ .../user_settings/userDeleteAccountForm.js | 81 ++++++ .../user_settings/userUpdateProfileForm.js | 84 ++++++ src/config.js | 20 ++ src/hooks/useDashboardList.js | 52 ++++ src/pages/dashboard.js | 42 ++- src/pages/dashboards/user.js | 209 ++------------- src/redux/asyncThunks/userAuthAsyncThunk.js | 175 ++++++++++++- src/redux/slices/userAuthSlice.js | 86 +++--- src/services/cookieService.js | 46 ++++ src/styles/general.scss | 247 +++++++++++------- 16 files changed, 1031 insertions(+), 561 deletions(-) create mode 100644 src/components/forms/user_settings/userChangePasswordForm.js create mode 100644 src/components/forms/user_settings/userDeleteAccountForm.js create mode 100644 src/components/forms/user_settings/userUpdateProfileForm.js create mode 100644 src/config.js create mode 100644 src/hooks/useDashboardList.js create mode 100644 src/services/cookieService.js diff --git a/package-lock.json b/package-lock.json index d28afd8..cda3fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.8.1", "gatsby": "^5.13.3", "gatsby-plugin-sass": "^6.14.0", + "js-cookie": "^3.0.5", "react": "^18", "react-dom": "^18", "react-redux": "^9.2.0", @@ -11751,6 +11752,15 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -25860,6 +25870,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index cb6cb73..cde5981 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "axios": "^1.8.1", "gatsby": "^5.13.3", "gatsby-plugin-sass": "^6.14.0", + "js-cookie": "^3.0.5", "react": "^18", "react-dom": "^18", "react-redux": "^9.2.0", diff --git a/src/components/forms/formGenerator.js b/src/components/forms/formGenerator.js index 69542da..e1b8867 100644 --- a/src/components/forms/formGenerator.js +++ b/src/components/forms/formGenerator.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import passwordVisibleImg from '../../images/password-visible.png' import passwordHiddenImg from '../../images/password-hidden.png' @@ -6,43 +6,28 @@ import passwordHiddenImg from '../../images/password-hidden.png' /** * * @param { [ {}, {}, ...{} ] } inputList - list of dicts with info about input - * @param { [] } refList - react ref objects list for handler validation * @param { } action - fetch method */ export const FormGenerator = ({ - inputList, refList, + inputList, action }) => { - const handler = async (event) => { - event.preventDefault() - - if ( inputList[0].action === 'Async' ) { - await action(refList) - } else if ( - inputList[0].action === 'Download' - || inputList[0].action === 'Upload' - ) { - await action() - } else { - for (let i = 0; i < refList.length; i++) { - if (refList[i].current.value === '' - && inputList[0].action !== 'Update' - || i === 0 - && refList.length !== 1 - ) { - refList[i].current.focus() - } else if (i === refList.length - 1) { - await action(refList) - } + const handleSubmit = (e) => { + e.preventDefault(); + const formData = {}; + inputList.forEach((singleInput, index) => { + if (typeof singleInput.type !== 'undefined' && singleInput.ref?.current) { + formData[singleInput.name] = singleInput.ref.current.value; } - } - } + }); + action(formData); + }; let info return ( -
handler(event)} className="form"> + { inputList.map((input, key) => { @@ -104,6 +89,13 @@ export const FormGenerator = ({ key={key} /> ) + } else if (input.type === 'label') { + return ( +
+ +
{input.value}
+
+ ) } }) } @@ -112,10 +104,10 @@ export const FormGenerator = ({ ? <> : } @@ -156,6 +148,7 @@ const TextInputGenerator = ({ } else { setTextInputValidationInfo("Success") } + input.validationInfo = textInputValidationInfo } return ( @@ -167,12 +160,13 @@ const TextInputGenerator = ({ id={input.name + info.action + info.endpoint + 'Input'} autoComplete='off' ref={input.ref} - onChange={ input.onChange === null ? defaultValidation : input.onChange} + onChange={input.onChange === null ? defaultValidation : input.onChange} className={ [ "Empty", "Success"].includes( input.validationInfo === null ? textInputValidationInfo : input.validationInfo ) ? "" : "input-incorrect" } + placeholder={input.placeholder === null ? "" : input.placeholder} />
{ input.validationInfo }
- ) @@ -465,7 +458,6 @@ const UploadInputGenerator = ({ const onLoadFile = async (event) => { event.preventDefault() let data = event.target.files[0] - // input.setFile(await toBase64(data)) input.setFile( data ) setDropInfos(data.name, data.size) } @@ -474,7 +466,6 @@ const UploadInputGenerator = ({ event.preventDefault() event.persist() let data = event.dataTransfer.files[0] - // input.setFile(await toBase64(data)) input.setFile( data ) setDropInfos(data.name, data.size) } diff --git a/src/components/forms/user_auth/userLogin.js b/src/components/forms/user_auth/userLogin.js index 9e15db8..7a422d0 100644 --- a/src/components/forms/user_auth/userLogin.js +++ b/src/components/forms/user_auth/userLogin.js @@ -1,121 +1,106 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, createRef, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { navigate } from 'gatsby'; +import { loginUser } from '../../../redux/asyncThunks/userAuthAsyncThunk'; +import { selectAuthError, selectAuthLoading, setError } from '../../../redux/slices/userAuthSlice'; +import FormGenerator from '../formGenerator'; -// import { useSelector, useDispatch } from 'react-redux' +const UserLogin = () => { + const dispatch = useDispatch(); + const error = useSelector(selectAuthError); + const loading = useSelector(selectAuthLoading); + const [infoMessage, setInfoMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); -// import { userAuthSelector } from '../../../redux/slices/userAuthSlice' -// import userAuthAsyncThunk from '../../../redux/asyncThunks/userAuthAsyncThunk' + const usernameInput = createRef(); + const passwordInput = createRef(); -import FormGenerator from '../formGenerator' + const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty"); + const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty"); + const [allowButtonAction, setAllowButtonAction] = useState(false); -const UserLoginForm = () => { + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/; - const usernameInput = React.createRef() - const passwordInput = React.createRef() - - const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/ - - const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty") - const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty") - - const [password, setPassword] = useState("") - - const [allowButtonAction, setAllowButtonAction] = useState(false) - const usernameValidation = (event) => { - if (event.target.value === "") { - setUsernameValidationInfo("Email is required.") - } else if(!emailRegex.test(event.target.value)) { - setUsernameValidationInfo("Please provide correct email") + setUsernameValidationInfo("Login jest wymagany"); } else { - setUsernameValidationInfo("Success") + setUsernameValidationInfo("Success"); } - } - + }; + const passwordValidation = (event) => { - - setPassword(event.target.value) - if (event.target.value === "") { - setPasswordValidationInfo("Password is required.") - } else if(!passwordRegex.test(event.target.value)) { - setPasswordValidationInfo("Password require:\n - At least 8 characters,\n - At least one uppercase letter,\n - At least one lowercase letter,\n - At least one digit,\n - At least one special character.") + setPasswordValidationInfo("Hasło jest wymagane"); + } else if (!passwordRegex.test(event.target.value)) { + setPasswordValidationInfo("Hasło nie spełnia wymagań tej witryny"); } else { - setPasswordValidationInfo("Success") + setPasswordValidationInfo("Success"); } - } - + }; + useEffect(() => { - setAllowButtonAction( - usernameValidationInfo === "Success" - && passwordValidationInfo === "Success" - ) - }, [ - allowButtonAction, - usernameValidationInfo, - passwordValidationInfo - ] - ) - - // const dispatch = useDispatch() - // const { info } = useSelector( userAuthSelector ) - const info = "" // if redux is integrated - delete this line + setAllowButtonAction( + usernameValidationInfo === "Success" && + passwordValidationInfo === "Success" + ); + }, [ + usernameValidationInfo, + passwordValidationInfo, + ]); - let refList = [ - usernameInput, - passwordInput - ] - - let inputList = [ + const inputList = [ { type: 'info', - action: 'Create', - endpint: 'user/auth', - button_value: 'SIGN IN' + action: 'Login', + endpoint: 'auth', + button_value: loading ? 'LOGOWANIE...' : 'ZALOGUJ', + allowButtonAction: allowButtonAction }, { type: 'text', - name: 'EMAIL', + name: 'LOGIN', ref: usernameInput, onChange: usernameValidation, validationInfo: usernameValidationInfo }, { type: 'password', - name: 'PASSWORD', + name: 'HASŁO', ref: passwordInput, onChange: passwordValidation, validationInfo: passwordValidationInfo } - ] + ]; - const login = async ( refs ) => { - let pass = { - username: refs[0].current.value, - password: refs[1].current.value + const login = async (formData) => { + try { + const credentials = { + username: formData.LOGIN, + password: formData.HASŁO + }; + + await dispatch(loginUser(credentials)).unwrap(); + setInfoMessage("Logowanie zakończone sukcesem!"); + navigate('/dashboard'); + } catch (error) { + setErrorMessage("Wystąpił błąd podczas logowania (" + error.message + ")"); } - // dispatch( - // userAuthAsyncThunk.fetchLogin( - // pass - // ) - // ) - } + }; return ( -
+
- { info } + {infoMessage &&
{infoMessage}
} + {errorMessage &&
{errorMessage}
}
- ) + ); +}; -} - -export default UserLoginForm +export default UserLogin; diff --git a/src/components/forms/user_auth/userRegister.js b/src/components/forms/user_auth/userRegister.js index f204d7d..ac0ec7d 100644 --- a/src/components/forms/user_auth/userRegister.js +++ b/src/components/forms/user_auth/userRegister.js @@ -1,152 +1,161 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, createRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { navigate } from 'gatsby'; +import { registerUser } from '../../../redux/asyncThunks/userAuthAsyncThunk'; +import { selectAuthError, selectAuthLoading } from '../../../redux/slices/userAuthSlice'; +import FormGenerator from '../formGenerator'; -// import { useSelector, useDispatch } from 'react-redux' +const UserRegister = () => { + const dispatch = useDispatch(); + const error = useSelector(selectAuthError); + const loading = useSelector(selectAuthLoading); + const [infoMessage, setInfoMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); -// import { userCrudSelector } from '../../../redux/slices/userCrudSlice' -// import userCrudAsyncThunk from '../../../redux/asyncThunks/userCrudAsyncThunk' + const usernameInput = createRef(); + const emailInput = createRef(); + const passwordInput = createRef(); + const confirmPasswordInput = createRef(); -import FormGenerator from '../formGenerator' + const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/; + const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty"); + const [emailValidationInfo, setEmailValidationInfo] = useState("Empty"); + const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty"); + const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty"); -const UserRegisterForm = () => { - - const usernameInput = React.createRef() - const passwordInput = React.createRef() - const confirmPasswordInput = React.createRef() - - const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/ - - const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty") - const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty") - const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty") - - const [password, setPassword] = useState("") - const [confirmPassword, setConfirmPassword] = useState("") - - const [allowButtonAction, setAllowButtonAction] = useState(false) + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [allowButtonAction, setAllowButtonAction] = useState(false); const usernameValidation = (event) => { if (event.target.value === "") { - setUsernameValidationInfo("Email is required.") - } else if(!emailRegex.test(event.target.value)) { - setUsernameValidationInfo("Please provide correct email") + setUsernameValidationInfo("Login jest wymagany"); } else { - setUsernameValidationInfo("Success") + setUsernameValidationInfo("Success"); } - } + }; + + const emailValidation = (event) => { + if (event.target.value === "") { + setEmailValidationInfo("Email jest wymagany"); + } else if (!emailRegex.test(event.target.value)) { + setEmailValidationInfo("Nieprawidłowy format emaila"); + } else { + setEmailValidationInfo("Success"); + } + }; const passwordValidation = (event) => { - - setPassword(event.target.value) + setPassword(event.target.value); if (event.target.value === "") { - setPasswordValidationInfo("Password is required.") - } else if(!passwordRegex.test(event.target.value)) { - setPasswordValidationInfo("Password require:\n - At least 8 characters,\n - At least one uppercase letter,\n - At least one lowercase letter,\n - At least one digit,\n - At least one special character.") + setPasswordValidationInfo("Hasło jest wymagane"); + } else if (!passwordRegex.test(event.target.value)) { + setPasswordValidationInfo("Hasło musi zawierać:\n - Minimum 8 znaków\n - Maksimum 24 znaki\n - Minimum jedną wielką literę\n - Minimum jedną małą literę\n - Minimum jedną cyfrę\n - Minimum jeden znak specjalny"); } else { - setPasswordValidationInfo("Success") + setPasswordValidationInfo("Success"); } - if(event.target.value !== confirmPassword) { - setConfirmPasswordValidationInfo("Passwords are different.") + if (event.target.value !== confirmPassword) { + setConfirmPasswordValidationInfo("Hasła nie są identyczne"); } else { - setConfirmPasswordValidationInfo("Success") + setConfirmPasswordValidationInfo("Success"); } - } + }; const confirmPasswordValidation = (event) => { + setConfirmPassword(event.target.value); - setConfirmPassword(event.target.value) - - if(event.target.value !== password) { - setConfirmPasswordValidationInfo("Passwords are different.") + if (event.target.value !== password) { + setConfirmPasswordValidationInfo("Hasła nie są identyczne"); } else { - setConfirmPasswordValidationInfo("Success") + setConfirmPasswordValidationInfo("Success"); } - } + }; useEffect(() => { - setAllowButtonAction( - usernameValidationInfo === "Success" - && passwordValidationInfo === "Success" - && confirmPasswordValidationInfo === "Success" - ) - }, [ - allowButtonAction, - usernameValidationInfo, - passwordValidationInfo, - confirmPasswordValidationInfo - ] - ) + setAllowButtonAction( + usernameValidationInfo === "Success" && + emailValidationInfo === "Success" && + passwordValidationInfo === "Success" && + confirmPasswordValidationInfo === "Success" + ); + }, [ + usernameValidationInfo, + emailValidationInfo, + passwordValidationInfo, + confirmPasswordValidationInfo + ]); - // const dispatch = useDispatch() - // const { info } = useSelector( userCrudSelector ) - const info = "" // if redux is integrated - delete this line - - let refList = [ - usernameInput, - passwordInput, - confirmPasswordInput - ] - - let inputList = [ + const inputList = [ { type: 'info', - action: 'Create', - endpint: 'user/auth/register', - button_value: 'SIGN UP', + action: 'Register', + endpoint: 'auth', + button_value: loading ? 'REJESTRACJA...' : 'ZAREJESTRUJ', allowButtonAction: allowButtonAction }, { type: 'text', - name: 'EMAIL', + name: 'LOGIN', ref: usernameInput, onChange: usernameValidation, validationInfo: usernameValidationInfo }, + { + type: 'text', + name: 'EMAIL', + ref: emailInput, + onChange: emailValidation, + validationInfo: emailValidationInfo + }, { type: 'password', - name: 'PASSWORD', + name: 'HASŁO', ref: passwordInput, onChange: passwordValidation, validationInfo: passwordValidationInfo }, { type: 'password', - name: 'CONFIRM PASSWORD', + name: 'POTWIERDŹ HASŁO', ref: confirmPasswordInput, onChange: confirmPasswordValidation, validationInfo: confirmPasswordValidationInfo } - ] + ]; - const register = async ( refs ) => { - let pass = { - username: refs[0].current.value, - password: refs[1].current.value + const register = async (formData) => { + try { + const userData = { + username: formData.LOGIN, + email: formData.EMAIL, + password: formData.HASŁO + }; + + await dispatch(registerUser(userData)).unwrap(); + setInfoMessage("Rejestracja zakończona sukcesem!"); + navigate('/dashboard'); + } catch (error) { + setErrorMessage("Wystąpił błąd podczas rejestracji (" + error.massage + ")"); } - // dispatch( - // userCrudAsyncThunk.fetchRegister( - // pass - // ) - // ) - } + }; return ( -
+
- { info } + {infoMessage &&
{infoMessage}
} + {errorMessage &&
{errorMessage}
}
- ) + ); +}; -} - -export default UserRegisterForm +export default UserRegister; diff --git a/src/components/forms/user_settings/userChangePasswordForm.js b/src/components/forms/user_settings/userChangePasswordForm.js new file mode 100644 index 0000000..6640cfa --- /dev/null +++ b/src/components/forms/user_settings/userChangePasswordForm.js @@ -0,0 +1,137 @@ +import React, { useState, createRef, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { changePassword } from '../../../redux/asyncThunks/userAuthAsyncThunk'; +import { selectAuthError, selectAuthLoading } from '../../../redux/slices/userAuthSlice'; +import FormGenerator from '../formGenerator'; + +const UserChangePasswordForm = () => { + const dispatch = useDispatch(); + const loading = useSelector(selectAuthLoading); + const error = useSelector(selectAuthError); + const [infoMessage, setInfoMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + + const currentPasswordInput = createRef(); + const newPasswordInput = createRef(); + const confirmPasswordInput = createRef(); + + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/; + + const [currentPasswordValidationInfo, setCurrentPasswordValidationInfo] = useState("Empty"); + const [newPasswordValidationInfo, setNewPasswordValidationInfo] = useState("Empty"); + const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty"); + + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [allowButtonAction, setAllowButtonAction] = useState(false); + + const currentPasswordValidation = (event) => { + if (event.target.value === "") { + setCurrentPasswordValidationInfo("Hasło jest wymagane"); + } else if (event.target.value === newPassword) { + setCurrentPasswordValidationInfo("Nowe hasło nie może być takie samo jak aktualne"); + } else { + setCurrentPasswordValidationInfo("Success"); + } + }; + + const newPasswordValidation = (event) => { + setNewPassword(event.target.value); + + if (event.target.value === "") { + setNewPasswordValidationInfo("Hasło jest wymagane"); + } else if (!passwordRegex.test(event.target.value)) { + setNewPasswordValidationInfo("Hasło musi zawierać:\n - Minimum 8 znaków\n - Maksimum 24 znaki\n - Minimum jedną wielką literę\n - Minimum jedną małą literę\n - Minimum jedną cyfrę\n - Minimum jeden znak specjalny"); + } else { + setNewPasswordValidationInfo("Success"); + } + + if (event.target.value !== confirmPassword) { + setConfirmPasswordValidationInfo("Hasła nie są identyczne"); + } else { + setConfirmPasswordValidationInfo("Success"); + } + }; + + const confirmPasswordValidation = (event) => { + setConfirmPassword(event.target.value); + + if (event.target.value !== newPassword) { + setConfirmPasswordValidationInfo("Hasła nie są identyczne"); + } else { + setConfirmPasswordValidationInfo("Success"); + } + }; + + useEffect(() => { + setAllowButtonAction( + currentPasswordValidationInfo === "Success" && + newPasswordValidationInfo === "Success" && + confirmPasswordValidationInfo === "Success" + ); + }, [ + currentPasswordValidationInfo, + newPasswordValidationInfo, + confirmPasswordValidationInfo + ]); + + const inputList = [ + { + type: 'info', + action: 'Update', + endpoint: 'User', + button_value: loading ? 'AKTUALIZACJA...' : 'AKTUALIZUJ', + allowButtonAction: allowButtonAction + }, + { + type: 'password', + name: 'AKTUALNE HASŁO', + ref: currentPasswordInput, + onChange: currentPasswordValidation, + validationInfo: currentPasswordValidationInfo + }, + { + type: 'password', + name: 'NOWE HASŁO', + ref: newPasswordInput, + onChange: newPasswordValidation, + validationInfo: newPasswordValidationInfo + }, + { + type: 'password', + name: 'POTWIERDŹ NOWE HASŁO', + ref: confirmPasswordInput, + onChange: confirmPasswordValidation, + validationInfo: confirmPasswordValidationInfo + } + ]; + + const update = async (formData) => { + try { + const userData = { + currentPassword: formData['AKTUALNE HASŁO'], + newPassword: formData['NOWE HASŁO'] + }; + + await dispatch(changePassword(userData)).unwrap(); + setInfoMessage("Hasło zostało zmienione!"); + } catch (error) { + setErrorMessage("Wystąpił błąd podczas zmiany hasła (" + error.message + ")"); + } + }; + + return ( +
+ +
+ {infoMessage &&
{infoMessage}
} + {errorMessage &&
{errorMessage}
} +
+
+ ); +}; + +export default UserChangePasswordForm; \ No newline at end of file diff --git a/src/components/forms/user_settings/userDeleteAccountForm.js b/src/components/forms/user_settings/userDeleteAccountForm.js new file mode 100644 index 0000000..41294d6 --- /dev/null +++ b/src/components/forms/user_settings/userDeleteAccountForm.js @@ -0,0 +1,81 @@ +import React, { useState, createRef, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { navigate } from 'gatsby'; +import { deleteAccount } from '../../../redux/asyncThunks/userAuthAsyncThunk'; +import { selectAuthError, selectAuthLoading } from '../../../redux/slices/userAuthSlice'; +import FormGenerator from '../formGenerator'; + +const UserDeleteAccountForm = () => { + const dispatch = useDispatch(); + const loading = useSelector(selectAuthLoading); + const error = useSelector(selectAuthError); + const [infoMessage, setInfoMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + + const deleteConfirmationInput = createRef(); + const [deleteConfirmationValidationInfo, setDeleteConfirmationValidationInfo] = useState("Empty"); + const [allowButtonAction, setAllowButtonAction] = useState(false); + + const deleteConfirmationValidation = (event) => { + if (event.target.value === "") { + setDeleteConfirmationValidationInfo("Wpisz 'DELETE' aby potwierdzić usunięcie konta"); + } else if (event.target.value !== "DELETE") { + setDeleteConfirmationValidationInfo("Wpisz dokładnie 'DELETE' aby potwierdzić usunięcie konta"); + } else { + setDeleteConfirmationValidationInfo("Success"); + } + }; + + useEffect(() => { + setAllowButtonAction(deleteConfirmationValidationInfo === "Success"); + }, [deleteConfirmationValidationInfo]); + + const inputList = [ + { + type: 'info', + action: 'Delete', + endpoint: 'User', + button_value: loading ? 'USUWANIE...' : 'USUŃ KONTO', + allowButtonAction: allowButtonAction + }, + { + type: 'text', + name: 'POTWIERDŹ USUNIĘCIE', + ref: deleteConfirmationInput, + onChange: deleteConfirmationValidation, + validationInfo: deleteConfirmationValidationInfo, + placeholder: "Wpisz 'DELETE' aby potwierdzić usunięcie konta" + } + ]; + + const deleteAccountHandler = async (formData) => { + try { + if (formData['POTWIERDŹ USUNIĘCIE'] === 'DELETE') { + await dispatch(deleteAccount()).unwrap(); + setInfoMessage("Konto zostało usunięte!"); + setTimeout(() => { + navigate('/'); + }, 2000); + } else { + setErrorMessage("Wpisz 'DELETE' aby potwierdzić usunięcie konta"); + } + } catch (error) { + setErrorMessage("Wystąpił błąd podczas usuwania konta (" + error.message + ")"); + } + }; + + return ( +
+ +
+ {infoMessage &&
{infoMessage}
} + {errorMessage &&
{errorMessage}
} +
+
+ ); +}; + +export default UserDeleteAccountForm; \ No newline at end of file diff --git a/src/components/forms/user_settings/userUpdateProfileForm.js b/src/components/forms/user_settings/userUpdateProfileForm.js new file mode 100644 index 0000000..c0441bc --- /dev/null +++ b/src/components/forms/user_settings/userUpdateProfileForm.js @@ -0,0 +1,84 @@ +import React, { useState, createRef, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { updateProfile } from '../../../redux/asyncThunks/userAuthAsyncThunk'; +import { selectAuthError, selectAuthLoading, selectCurrentUser } from '../../../redux/slices/userAuthSlice'; +import FormGenerator from '../formGenerator'; + +const UserUpdateProfileForm = () => { + const dispatch = useDispatch(); + const user = useSelector(selectCurrentUser); + const loading = useSelector(selectAuthLoading); + const error = useSelector(selectAuthError); + const [infoMessage, setInfoMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + + const emailInput = createRef(); + const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const [emailValidationInfo, setEmailValidationInfo] = useState("Empty"); + const [allowButtonAction, setAllowButtonAction] = useState(false); + + const emailValidation = (event) => { + if (event.target.value === "") { + setEmailValidationInfo("Email jest wymagany"); + } else if (!emailRegex.test(event.target.value)) { + setEmailValidationInfo("Nieprawidłowy format emaila"); + } else { + setEmailValidationInfo("Success"); + } + }; + + useEffect(() => { + setAllowButtonAction(emailValidationInfo === "Success"); + }, [emailValidationInfo]); + + const inputList = [ + { + type: 'info', + action: 'Update', + endpoint: 'User', + button_value: loading ? 'AKTUALIZACJA...' : 'AKTUALIZUJ', + allowButtonAction: allowButtonAction + }, + { + type: 'label', + name: 'LOGIN', + value: user?.login || '' + }, + { + type: 'text', + name: 'EMAIL', + ref: emailInput, + onChange: emailValidation, + validationInfo: emailValidationInfo, + value: user?.email || '' + } + ]; + + const update = async (formData) => { + try { + const userData = { + email: formData.EMAIL + }; + + await dispatch(updateProfile(userData)).unwrap(); + setInfoMessage("Profil został zaktualizowany!"); + } catch (error) { + setErrorMessage("Wystąpił błąd podczas aktualizacji (" + error.message + ")"); + } + }; + + return ( +
+ +
+ {infoMessage &&
{infoMessage}
} + {errorMessage &&
{errorMessage}
} +
+
+ ); +}; + +export default UserUpdateProfileForm; \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..7e5d2a8 --- /dev/null +++ b/src/config.js @@ -0,0 +1,20 @@ +// Konfiguracja API +export const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api'; + +// Konfiguracja aplikacji +export const APP_CONFIG = { + name: 'Render App', + version: '1.0.0', + environment: process.env.NODE_ENV || 'development' +}; + +// Konfiguracja autentykacji +export const AUTH_CONFIG = { + tokenExpiry: 7, // dni + refreshTokenExpiry: 30, // dni + cookieOptions: { + secure: true, + sameSite: 'strict', + path: '/' + } +}; \ No newline at end of file diff --git a/src/hooks/useDashboardList.js b/src/hooks/useDashboardList.js new file mode 100644 index 0000000..32ee413 --- /dev/null +++ b/src/hooks/useDashboardList.js @@ -0,0 +1,52 @@ +import { useState } from 'react'; + +export const useDashboardList = (initialItems = []) => { + const [selectedItem, setSelectedItem] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [items, setItems] = useState(initialItems); + const [isFormVisible, setIsFormVisible] = useState(false); + const [formMode, setFormMode] = useState('create'); + const [message, setMessage] = useState({ type: '', text: '' }); + + const handleSearch = (query) => { + setSearchQuery(query); + // Implementacja wyszukiwania w zależności od typu elementów + const filteredItems = initialItems.filter(item => + Object.values(item).some(value => + String(value).toLowerCase().includes(query.toLowerCase()) + ) + ); + setItems(filteredItems); + }; + + const handleItemSelect = (item) => { + setSelectedItem(item); + }; + + const handleFormToggle = (mode = 'create', item = null) => { + setFormMode(mode); + setSelectedItem(item); + setIsFormVisible(!isFormVisible); + }; + + const handleMessage = (type, text) => { + setMessage({ type, text }); + setTimeout(() => { + setMessage({ type: '', text: '' }); + }, 3000); + }; + + return { + selectedItem, + searchQuery, + items, + isFormVisible, + formMode, + message, + handleSearch, + handleItemSelect, + handleFormToggle, + handleMessage, + setItems + }; +}; \ No newline at end of file diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js index da19c62..3f02113 100644 --- a/src/pages/dashboard.js +++ b/src/pages/dashboard.js @@ -1,21 +1,36 @@ -import React, { useState } from 'react'; -import '../styles/general.scss'; -import '@fortawesome/fontawesome-free/css/all.min.css'; +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { navigate } from 'gatsby'; +import { logoutUser, checkAuth } from '../redux/asyncThunks/userAuthAsyncThunk'; +import { selectIsAuthenticated, selectUserPermissions } from '../redux/slices/userAuthSlice'; -import FootComponent from '../components/foot.js'; import NavBarComponent from '../components/navbar.js'; - -import ThreeDModelsDashboard from './dashboards/3d-models.js'; +import FootComponent from '../components/foot.js'; import AIModelsDashboard from './dashboards/ai.models.js'; import AITasksDashboard from './dashboards/ai.tasks.js'; import RendersDashboard from './dashboards/renders.js'; import ServersDashboard from './dashboards/servers.js'; -import UserSettings from './dashboards/user.js'; +import UserSettingsDashboard from './dashboards/user.js'; +import ThreeDModelsDashboard from './dashboards/3d-models.js'; const DashboardPage = () => { + const dispatch = useDispatch(); + const isAuthenticated = useSelector(selectIsAuthenticated); + const permissions = useSelector(selectUserPermissions); + const icons_size = "fa-2x"; const [activeComponent, setActiveComponent] = useState('3d-models'); + useEffect(() => { + if (!isAuthenticated) { + dispatch(checkAuth()) + .unwrap() + .catch(() => { + navigate('/auth/login'); + }); + } + }, [dispatch, isAuthenticated]); + const handleNavigation = (path) => { setActiveComponent(path); }; @@ -24,10 +39,13 @@ const DashboardPage = () => { return activeComponent === path; }; - const handleLogout = () => { - // TODO: Implement proper logout logic (clear tokens, session, etc.) - console.log('Logging out...'); - window.location.href = '/login'; + const handleLogout = async () => { + try { + await dispatch(logoutUser()).unwrap(); + navigate('/auth/login'); + } catch (error) { + console.error('Błąd podczas wylogowywania:', error); + } }; const renderContent = () => { @@ -41,7 +59,7 @@ const DashboardPage = () => { case 'servers': return ; case 'settings': - return ; + return ; case '3d-models': default: return ; diff --git a/src/pages/dashboards/user.js b/src/pages/dashboards/user.js index f7c6a42..6d9a3dd 100644 --- a/src/pages/dashboards/user.js +++ b/src/pages/dashboards/user.js @@ -1,193 +1,42 @@ -import React, { useState, useRef } from 'react'; -import FormGenerator from '../../components/forms/formGenerator'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { selectCurrentUser } from '../../redux/slices/userAuthSlice'; +import UserUpdateProfileForm from '../../components/forms/user_settings/userUpdateProfileForm'; +import UserChangePasswordForm from '../../components/forms/user_settings/userChangePasswordForm'; +import UserDeleteAccountForm from '../../components/forms/user_settings/userDeleteAccountForm'; -const UserSettings = () => { - const [isEditing, setIsEditing] = useState(false); - const [message, setMessage] = useState({ type: '', text: '' }); - - const usernameInput = useRef(); - const emailInput = useRef(); - const currentPasswordInput = useRef(); - const newPasswordInput = useRef(); - const confirmPasswordInput = useRef(); - - const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty"); - const [emailValidationInfo, setEmailValidationInfo] = useState("Empty"); - const [currentPasswordValidationInfo, setCurrentPasswordValidationInfo] = useState("Empty"); - const [newPasswordValidationInfo, setNewPasswordValidationInfo] = useState("Empty"); - const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty"); - - const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; - - const usernameValidation = (event) => { - if (event.target.value === "") { - setUsernameValidationInfo("Username is required."); - } else { - setUsernameValidationInfo("Success"); - } - }; - - const emailValidation = (event) => { - if (event.target.value === "") { - setEmailValidationInfo("Email is required."); - } else if (!emailRegex.test(event.target.value)) { - setEmailValidationInfo("Please provide correct email"); - } else { - setEmailValidationInfo("Success"); - } - }; - - const currentPasswordValidation = (event) => { - if (event.target.value === "") { - setCurrentPasswordValidationInfo("Current password is required."); - } else { - setCurrentPasswordValidationInfo("Success"); - } - }; - - const newPasswordValidation = (event) => { - if (event.target.value === "") { - setNewPasswordValidationInfo("New password is required."); - } else if (!passwordRegex.test(event.target.value)) { - setNewPasswordValidationInfo("Password require:\n - At least 8 characters,\n - At least one uppercase letter,\n - At least one lowercase letter,\n - At least one digit,\n - At least one special character."); - } else { - setNewPasswordValidationInfo("Success"); - } - }; - - const confirmPasswordValidation = (event) => { - if (event.target.value === "") { - setConfirmPasswordValidationInfo("Please confirm your new password."); - } else if (event.target.value !== newPasswordInput.current.value) { - setConfirmPasswordValidationInfo("Passwords do not match."); - } else { - setConfirmPasswordValidationInfo("Success"); - } - }; - - const handleSubmit = async (refs) => { - try { - // TODO: Implement API call to update user data - console.log('Updating user data:', { - username: refs[0].current.value, - email: refs[1].current.value, - currentPassword: refs[2].current.value, - newPassword: refs[3].current.value - }); - - setMessage({ type: 'success', text: 'Settings updated successfully!' }); - setIsEditing(false); - } catch (error) { - setMessage({ type: 'error', text: 'Failed to update settings. Please try again.' }); - } - }; - - const getInputList = () => { - const baseInputs = [ - { - type: 'info', - action: 'Update', - endpoint: 'user/settings', - button_value: isEditing ? 'SAVE CHANGES' : '' - }, - { - type: 'text', - name: 'USERNAME', - ref: usernameInput, - onChange: usernameValidation, - validationInfo: usernameValidationInfo, - disabled: !isEditing - }, - { - type: 'text', - name: 'EMAIL', - ref: emailInput, - onChange: emailValidation, - validationInfo: emailValidationInfo, - disabled: !isEditing - } - ]; - - if (isEditing) { - baseInputs.push( - { - type: 'password', - name: 'CURRENT PASSWORD', - ref: currentPasswordInput, - onChange: currentPasswordValidation, - validationInfo: currentPasswordValidationInfo - }, - { - type: 'password', - name: 'NEW PASSWORD', - ref: newPasswordInput, - onChange: newPasswordValidation, - validationInfo: newPasswordValidationInfo - }, - { - type: 'password', - name: 'CONFIRM NEW PASSWORD', - ref: confirmPasswordInput, - onChange: confirmPasswordValidation, - validationInfo: confirmPasswordValidationInfo - } - ); - } - - return baseInputs; - }; - - const formRefs = [usernameInput, emailInput, currentPasswordInput, newPasswordInput, confirmPasswordInput]; - - const handleFormAction = (refs) => { - const formData = {}; - formRefs.forEach((ref, index) => { - formData[getInputList()[index].name] = ref.current.value; - }); - handleSubmit(formRefs); - }; +const UserSettingsDashboard = () => { + const user = useSelector(selectCurrentUser); return ( -
-
-
-

User Settings

- +
+
+

Panel Użytkownika

+
+ Zalogowany jako: {user?.login}
+
- {message.text && ( -
- {message.text} +
+
+
+

Aktualizacja profilu

+
- )} - {isEditing && ( -
-
- ({ - ...field, - ref: formRefs[index], - type: field.type, - name: field.name, - onChange: field.onChange, - validationInfo: field.validationInfo - }))} - refList={formRefs} - action={handleFormAction} - /> -
+
+

Zmiana hasła

+
- )} + +
+

Usuwanie konta

+ +
+
); }; -export default UserSettings; +export default UserSettingsDashboard; diff --git a/src/redux/asyncThunks/userAuthAsyncThunk.js b/src/redux/asyncThunks/userAuthAsyncThunk.js index 55264dd..28d033e 100644 --- a/src/redux/asyncThunks/userAuthAsyncThunk.js +++ b/src/redux/asyncThunks/userAuthAsyncThunk.js @@ -1,32 +1,179 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import axios from 'axios'; - -const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000'; +import { setCredentials, setLoading, setError, logout } from '../slices/userAuthSlice'; +import { API_URL, AUTH_CONFIG } from '../../config'; +import { cookieService } from '../../services/cookieService'; export const loginUser = createAsyncThunk( 'userAuth/login', - async (credentials) => { - const formData = new FormData(); - formData.append('username', credentials.username); - formData.append('password', credentials.password); + async (credentials, { dispatch }) => { + try { + dispatch(setLoading(true)); + const response = await fetch(`${API_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(credentials), + }); - const response = await axios.post(`${API_URL}/auth`, formData); - return response.data; + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Błąd logowania'); + } + + dispatch(setCredentials({ + user: data.user, + token: data.token, + permissions: data.permissions + })); + + return data; + } catch (error) { + dispatch(setError(error.message)); + throw error; + } finally { + dispatch(setLoading(false)); + } } ); export const registerUser = createAsyncThunk( 'userAuth/register', - async (userData) => { - const response = await axios.post(`${API_URL}/register`, userData); - return response.data; + async (userData, { dispatch }) => { + try { + dispatch(setLoading(true)); + const response = await fetch(`${API_URL}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Błąd rejestracji'); + } + + dispatch(setCredentials({ + user: data.user, + token: data.token, + permissions: data.permissions + })); + + return data; + } catch (error) { + dispatch(setError(error.message)); + throw error; + } finally { + dispatch(setLoading(false)); + } + } +); + +export const logoutUser = createAsyncThunk( + 'userAuth/logout', + async (_, { dispatch }) => { + try { + dispatch(setLoading(true)); + const token = cookieService.getToken(); + + if (token) { + await fetch(`${API_URL}/auth/logout`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + } + + dispatch(logout()); + } catch (error) { + console.error('Błąd podczas wylogowywania:', error); + } finally { + dispatch(setLoading(false)); + } + } +); + +export const checkAuth = createAsyncThunk( + 'userAuth/checkAuth', + async (_, { dispatch }) => { + try { + const token = cookieService.getToken(); + + if (!token) { + throw new Error('Brak tokenu'); + } + + const response = await fetch(`${API_URL}/auth/verify`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Token nieprawidłowy'); + } + + const data = await response.json(); + return data; + } catch (error) { + dispatch(logout()); + throw error; + } } ); export const changePassword = createAsyncThunk( 'userAuth/changePassword', async (passwordData) => { - const response = await axios.post(`${API_URL}/change-password`, passwordData); + const response = await fetch(`${API_URL}/change-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(passwordData), + }); return response.data; } -); \ No newline at end of file +); + +export const updateProfile = createAsyncThunk( + 'userAuth/updateProfile', + async (profileData, { rejectWithValue }) => { + try { + const response = await fetch(`${API_URL}/user/profile`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${cookieService.getToken()}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(profileData), + }); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data); + } + } +); + +export const deleteAccount = createAsyncThunk( + 'userAuth/deleteAccount', + async (_, { rejectWithValue }) => { + try { + const response = await fetch(`${API_URL}/user/delete`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${cookieService.getToken()}`, + }, + }); + cookieService.clearAll(); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data); + } + } +); \ No newline at end of file diff --git a/src/redux/slices/userAuthSlice.js b/src/redux/slices/userAuthSlice.js index a6efb00..3a25f11 100644 --- a/src/redux/slices/userAuthSlice.js +++ b/src/redux/slices/userAuthSlice.js @@ -1,72 +1,54 @@ import { createSlice } from '@reduxjs/toolkit'; -import { loginUser, registerUser, changePassword } from '../asyncThunks/userAuthAsyncThunk'; +import { cookieService } from '../../services/cookieService'; const initialState = { - user: null, - token: null, - isLoading: false, - error: null + user: cookieService.getUserData() || null, + token: cookieService.getToken() || null, + isAuthenticated: !!cookieService.getToken(), + loading: false, + error: null, + permissions: [] }; const userAuthSlice = createSlice({ name: 'userAuth', initialState, reducers: { + setCredentials: (state, { payload }) => { + const { user, token, permissions } = payload; + state.user = user; + state.token = token; + state.isAuthenticated = true; + state.permissions = permissions; + cookieService.setToken(token); + cookieService.setUserData({ ...user, permissions }); + }, logout: (state) => { state.user = null; state.token = null; - state.error = null; + state.isAuthenticated = false; + state.permissions = []; + cookieService.clearAll(); + }, + setLoading: (state, { payload }) => { + state.loading = payload; + }, + setError: (state, { payload }) => { + state.error = payload; }, clearError: (state) => { state.error = null; } - }, - extraReducers: (builder) => { - // Login - builder.addCase(loginUser.pending, (state) => { - state.isLoading = true; - state.error = null; - }); - builder.addCase(loginUser.fulfilled, (state, action) => { - state.isLoading = false; - state.user = action.payload.user; - state.token = action.payload.token; - }); - builder.addCase(loginUser.rejected, (state, action) => { - state.isLoading = false; - state.error = action.error.message; - }); - - // Register - builder.addCase(registerUser.pending, (state) => { - state.isLoading = true; - state.error = null; - }); - builder.addCase(registerUser.fulfilled, (state, action) => { - state.isLoading = false; - state.user = action.payload.user; - state.token = action.payload.token; - }); - builder.addCase(registerUser.rejected, (state, action) => { - state.isLoading = false; - state.error = action.error.message; - }); - - // Change Password - builder.addCase(changePassword.pending, (state) => { - state.isLoading = true; - state.error = null; - }); - builder.addCase(changePassword.fulfilled, (state) => { - state.isLoading = false; - }); - builder.addCase(changePassword.rejected, (state, action) => { - state.isLoading = false; - state.error = action.error.message; - }); } }); -export const { logout, clearError } = userAuthSlice.actions; -export const userAuthSelector = (state) => state.userAuth; +export const { setCredentials, logout, setLoading, setError, clearError } = userAuthSlice.actions; + +export const selectCurrentUser = (state) => state.userAuth.user; +export const selectCurrentToken = (state) => state.userAuth.token; +export const selectIsAuthenticated = (state) => state.userAuth.isAuthenticated; +export const selectUserPermissions = (state) => state.userAuth.permissions; +export const selectAuthLoading = (state) => state.userAuth.loading; +export const selectAuthError = (state) => state.userAuth.error; + export default userAuthSlice.reducer; \ No newline at end of file diff --git a/src/services/cookieService.js b/src/services/cookieService.js new file mode 100644 index 0000000..a96229a --- /dev/null +++ b/src/services/cookieService.js @@ -0,0 +1,46 @@ +import Cookies from 'js-cookie'; + +const TOKEN_COOKIE_NAME = 'auth_token'; +const USER_COOKIE_NAME = 'user_data'; + +export const cookieService = { + setToken: (token) => { + Cookies.set(TOKEN_COOKIE_NAME, token, { + expires: 7, // 7 dni + secure: true, // tylko HTTPS + sameSite: 'strict', // ochrona przed CSRF + path: '/' + }); + }, + + getToken: () => { + return Cookies.get(TOKEN_COOKIE_NAME); + }, + + removeToken: () => { + Cookies.remove(TOKEN_COOKIE_NAME, { path: '/' }); + }, + + setUserData: (userData) => { + Cookies.set(USER_COOKIE_NAME, JSON.stringify(userData), { + expires: 7, + secure: true, + sameSite: 'strict', + path: '/' + }); + }, + + getUserData: () => { + const userData = Cookies.get(USER_COOKIE_NAME); + return userData ? JSON.parse(userData) : null; + }, + + removeUserData: () => { + Cookies.remove(USER_COOKIE_NAME, { path: '/' }); + }, + + clearAll: () => { + this.removeToken(); + this.removeUserData(); + } +}; \ No newline at end of file diff --git a/src/styles/general.scss b/src/styles/general.scss index d8eb7d7..8e92d84 100644 --- a/src/styles/general.scss +++ b/src/styles/general.scss @@ -1,3 +1,5 @@ +@import '@fortawesome/fontawesome-free/css/all.min.css'; + // colors $first-color: rgba(0, 90, 25, 1); @@ -7,6 +9,7 @@ $error-color: #dc3545; $in-progress-color: #ffa500; $queued-color: #fbff00; $success-color: $first-color; +$card-background: rgba(0, 0, 0, 0.2); $title-color: white; $subtitle-color: #a6a6a6; @@ -805,6 +808,13 @@ body, html { filter: invert(8%) sepia(63%) saturate(4888%) hue-rotate(8deg) brightness(113%) contrast(112%); } } + + .label-value { + width: 90%; + padding: 5%; + color: $subtitle-color; + font-size: 14px; + } } .popup { @@ -980,12 +990,6 @@ body, html { margin-top: 30px; } - .form_info { - float: float; - position: absolute; - margin-top: 0px; - } - } .float_form_render_sync { @@ -1146,119 +1150,108 @@ body, html { } .user-settings { - padding: 20px; - color: $secondary-color; + padding: 20px; + max-width: 100%; + margin: 0 auto; - .settings-header { + .settings-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; + margin-bottom: 30px; h2 { - color: $title-color; - margin: 0; - font-size: 24px; + margin: 0; + color: $title-color; + font-size: 24px; } - .edit-button { - background: $first-color; - color: white; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; + .settings-actions { + display: flex; + gap: 16px; - &.cancel { - background: #dc3545; - } + .edit-button { + padding: 8px 16px; + border: none; + border-radius: 4px; + background: $first-color; + color: white; + cursor: pointer; + transition: background-color 0.3s; - &:hover { - transform: translateY(-2px); - &.cancel { - background: darken(#dc3545, 10%); + &:hover { + background: darken($first-color, 10%); + } + + &.cancel { + background: $error-color; + + &:hover { + background: darken($error-color, 10%); + } + } } - &:not(.cancel) { - background: darken($first-color, 10%); + + .delete-button { + padding: 8px 16px; + border: none; + border-radius: 4px; + background: $error-color; + color: white; + cursor: pointer; + transition: background-color 0.3s; + + &:hover { + background: darken($error-color, 10%); + } } - } } - } + } - .message { - padding: 10px; + .settings-sections-container { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 20px; + + .settings-section { + flex: 1; + width: 450px; + background: $card-background; + border-radius: 8px; + padding: 20px; + + h3 { + margin: 0 0 20px 0; + color: $title-color; + font-size: 18px; + } + } + } + + .settings-section.delete-section { + border: 1px solid $error-color; + background: rgba($error-color, 0.1); + } + + .message { + padding: 12px; + border-radius: 4px; margin-bottom: 20px; - border-radius: 5px; - text-align: center; - font-weight: 500; &.success { - background: rgba(0, 120, 0, 0.2); - color: #00ff00; + background: rgba($success-color, 0.1); + color: $success-color; + border: 1px solid $success-color; } &.error { - background: rgba(220, 53, 69, 0.2); - color: #ff4444; + background: rgba($error-color, 0.1); + color: $error-color; + border: 1px solid $error-color; } - } - - .settings-form { - max-width: 500px; - margin: 0 auto; - - .form-group { - margin-bottom: 20px; - - label { - display: block; - margin-bottom: 8px; - color: $title-color; - font-weight: 600; - } - - input { - width: 100%; - padding: 10px; - background: rgba(0, 0, 0, 0.2); - border: 1px solid $border-color; - border-radius: 5px; - color: $secondary-color; - transition: all 0.3s ease; - - &:disabled { - background: rgba(0, 0, 0, 0.1); - cursor: not-allowed; - } - - &:focus { - outline: none; - border-color: $first-color; - box-shadow: 0 0 0 2px rgba($first-color, 0.2); - } - } - } - - .save-button { - width: 100%; - padding: 12px; - background: $first-color; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; - - &:hover { - background: darken($first-color, 10%); - transform: translateY(-2px); - } - } - } } +} .task-row { display: flex; @@ -1557,3 +1550,63 @@ body, html { .dashboard-content { min-height: 100%; /* Wymusza pojawienie się suwaka */ } + +.form-container { + margin: 0 auto; + padding: 20px; + border-radius: 8px; + + h2 { + color: $title-color; + text-align: center; + margin-bottom: 20px; + } + + .form_info { + float: float; + width: 400px; + height: 100px; + margin-top: 10px; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + + .success-message { + background: rgba($success-color, 0.1); + color: $success-color; + padding: 10px; + border-radius: 4px; + margin-bottom: 20px; + text-align: center; + border: 1px solid $success-color; + } + + .error-message { + background: rgba($error-color, 0.1); + color: $error-color; + padding: 10px; + border-radius: 4px; + margin-bottom: 20px; + text-align: center; + border: 1px solid $error-color; + } + + } +} + +.logout-button { + width: 100%; + padding: 10px; + background: $error-color; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s; + + &:hover { + background: darken($error-color, 10%); + } +}