diff --git a/client/package-lock.json b/client/package-lock.json index e1fb4f20..5f74488a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "react-dom": "^18.2.0", "react-redux": "^9.0.2", "react-router-dom": "^6.21.0", + "react-toastify": "^9.1.3", "sass": "^1.69.5" }, "devDependencies": { @@ -1285,6 +1286,14 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3151,6 +3160,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/client/package.json b/client/package.json index 459e6d93..78f1aeeb 100644 --- a/client/package.json +++ b/client/package.json @@ -10,15 +10,15 @@ "preview": "vite preview" }, "dependencies": { - "@reduxjs/toolkit": "^2.0.1", "@heroicons/react": "^2.0.18", + "@reduxjs/toolkit": "^2.0.1", "axios": "^1.6.2", "normalize.css": "^8.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-redux": "^9.0.2", "react-router-dom": "^6.21.0", + "react-toastify": "^9.1.3", "sass": "^1.69.5" }, "devDependencies": { diff --git a/client/src/App.jsx b/client/src/App.jsx index 042e44ef..470df060 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,11 +4,21 @@ import "./theme.scss"; import "./assets/styles/global/global.scss"; // Import Routes import Routes from "./routes"; +// Import Store +import store from "./redux/store"; +// Import Provider +import { Provider } from "react-redux"; +// Import Toastify +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; function App() { return ( <> - + + + + ); } diff --git a/client/src/assets/styles/components/Button.scss b/client/src/assets/styles/components/Button.scss index e42aef2e..9af0126b 100644 --- a/client/src/assets/styles/components/Button.scss +++ b/client/src/assets/styles/components/Button.scss @@ -4,6 +4,10 @@ padding: 12px 20px; border-radius: 6px; cursor: pointer; + color: inherit; + font-size: 19px; + font-weight: 500; + text-align: center; a { color: inherit; @@ -23,7 +27,7 @@ &--medium { a { - font-size: 1rem; + font-size: 1rem; } } diff --git a/client/src/components/common/Button.jsx b/client/src/components/common/Button.jsx index 0d501194..861f338c 100644 --- a/client/src/components/common/Button.jsx +++ b/client/src/components/common/Button.jsx @@ -8,9 +8,17 @@ export default function Button(props) { const { children, className, linkTo, ...rest } = props; return ( - + <> + {linkTo ? ( + + {children} + + ) : ( + + )} + ); } diff --git a/client/src/components/login/logIn.jsx b/client/src/components/login/logIn.jsx index d2aec56b..4b98d8ff 100644 --- a/client/src/components/login/logIn.jsx +++ b/client/src/components/login/logIn.jsx @@ -1,26 +1,74 @@ +import { useState, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Link, useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; import Button from "../common/Button"; +import TextInput from "../common/Inputs"; import "./logIn.scss"; +import { useLoginMutation } from "../../redux/slices/usersApiSlice"; +import { setCredentials } from "../../redux/slices/authSlice"; export default function LogIn() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const [login, { isLoading, error }] = useLoginMutation(); + + const { userInfo } = useSelector((state) => state.auth); + + useEffect(() => { + if (userInfo) { + /* TODO: Add later the home page not the landing page */ + navigate("/"); + } + }, [navigate, userInfo]); + + const submitHandler = async (e) => { + e.preventDefault(); + try { + const res = await login({ email, password }).unwrap(); + dispatch(setCredentials({ ...res?.data })); + navigate("/"); + } catch (err) { + toast.error(err?.data?.message || err.error); + console.error(err); + } + }; + return (
-
+

تسجيل الدخول

- - -
-
ليس لديك حساب؟
-
+ + ليس لديك حساب؟ + +
); } diff --git a/client/src/components/login/logIn.scss b/client/src/components/login/logIn.scss index 03497c7c..9af96fbb 100644 --- a/client/src/components/login/logIn.scss +++ b/client/src/components/login/logIn.scss @@ -15,13 +15,14 @@ background-color: var(--grey-800); border-radius: 1.875rem; display: flex; - width: 16.9375rem; padding: 1.0625rem 0rem; margin-top: 3rem; flex-direction: column; justify-content: center; align-items: center; - gap: 0.875rem; + gap: 1rem; + padding: 3rem 1rem; + .input-field { display: flex; width: 14.16319rem; diff --git a/client/src/components/signup/signUp.jsx b/client/src/components/signup/signUp.jsx index 7e7c74ef..5df44a01 100644 --- a/client/src/components/signup/signUp.jsx +++ b/client/src/components/signup/signUp.jsx @@ -1,17 +1,73 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Link, useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; import Button from "../common/Button"; import TextInput, { RadioInput } from "../common/Inputs"; import "./signUp.scss"; +import { useSignupMutation } from "../../redux/slices/usersApiSlice"; +import { setCredentials } from "../../redux/slices/authSlice"; export default function SignUp() { const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [middleName, setMiddleName] = useState(""); const [gender, setGender] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [rePassword, setRePassword] = useState(""); + const [phone, setPhone] = useState(""); + + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const [signup, { isLoading, error }] = useSignupMutation(); + + const { userInfo } = useSelector((state) => state.auth); + + useEffect(() => { + if (userInfo) { + /* TODO: Add later the home page not the landing page */ + navigate("/"); + } + }, [navigate, userInfo]); + + const submitHandler = async (e) => { + e.preventDefault(); + if (password !== rePassword) { + toast.error("الرمز السري غير متطابق"); + return; + } + try { + console.log({ + firstName, + middleName, + lastName, + password, + email, + phone, + gender: gender == "ذكر" ? "male" : "female", + }); + const res = await signup({ + firstName, + middleName, + lastName, + phone, + email, + password, + gender: gender == "ذكر" ? "male" : "female", + }).unwrap(); + dispatch(setCredentials({ ...res?.data })); + navigate("/"); + } catch (err) { + toast.error(err?.data?.message || err.error); + console.log(err?.data?.message || err.error); + } + }; return (
-
+

تسجيل حساب

@@ -23,65 +79,82 @@ export default function SignUp() { value={firstName} placeholder="أكتب أسمك الاول" onChange={(e) => setFirstName(e.target.value)} + required={true} /> - setMiddleName(e.target.value)} + required={true} + /> + setLastName(e.target.value)} required={true} - valuesArr={["أنثى", "ذكر"]} - onChange={(e) => setGender(e.target.value) } /> - - -
معلومات الحساب
- - - + setEmail(e.target.value)} + required={true} + /> + setPassword(e.target.value)} + required={true} + /> + setRePassword(e.target.value)} + required={true} + />
معلومات أخرى
- - + setPhone(e.target.value)} + required={true} + /> + setGender(e.target.value)} + required={true} + />
- + {isLoading &&

جاري التحميل...

} +
-
+
); } diff --git a/client/src/components/signup/signUp.scss b/client/src/components/signup/signUp.scss index bf589225..fff4b7b8 100644 --- a/client/src/components/signup/signUp.scss +++ b/client/src/components/signup/signUp.scss @@ -16,6 +16,7 @@ padding: 1.5rem 0.4375rem; flex-direction: column; justify-content: center; + align-items: center; gap: 1.0625rem; } @@ -23,13 +24,12 @@ background-color: var(--grey-800); border-radius: 1.875rem; display: flex; - width: 16.9375rem; - padding: 1.0625rem 0rem; + padding: 1.5rem 1rem; flex-direction: column; justify-content: center; align-items: center; gap: 0.875rem; - padding: 10px; + .input-field { display: flex; width: 14.16319rem; diff --git a/client/src/redux/slices/apiSlice.js b/client/src/redux/slices/apiSlice.js new file mode 100644 index 00000000..df5ff8e9 --- /dev/null +++ b/client/src/redux/slices/apiSlice.js @@ -0,0 +1,9 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; + +const baseQuery = fetchBaseQuery({ baseUrl: "" }); + +export const apiSlice = createApi({ + baseQuery, + tagTypes: ["Scouts"], + endpoints: () => ({}), +}); diff --git a/client/src/redux/slices/authSlice.js b/client/src/redux/slices/authSlice.js new file mode 100644 index 00000000..fc61df3e --- /dev/null +++ b/client/src/redux/slices/authSlice.js @@ -0,0 +1,27 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + userInfo: localStorage.getItem("userInfo") + ? JSON.parse(localStorage.getItem("userInfo")) + : null, +}; + +const authSlice = createSlice({ + name: "auth", + initialState, + reducers: { + setCredentials: (state, action) => { + state.userInfo = action.payload; + localStorage.setItem("userInfo", JSON.stringify(action.payload)); + }, + clearCredentials: (state, action) => { + action; + state.userInfo = null; + localStorage.removeItem("userInfo"); + }, + }, +}); + +export const { setCredentials, clearCredentials } = authSlice.actions; + +export default authSlice.reducer; diff --git a/client/src/redux/slices/userSlice.js b/client/src/redux/slices/userSlice.js deleted file mode 100644 index f5f3e2af..00000000 --- a/client/src/redux/slices/userSlice.js +++ /dev/null @@ -1,27 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit" - -const initialState = { - loggedIn: false, - email: '', -} - -const userSlice = createSlice({ - name: 'user', - initialState, - reducers: { - login: (state, action) => { - state.loggedIn = true; - state.email = action.payload.email; - }, - logout: (state, action) => { - state.loggedIn = false; - state.email = ''; - } - } -}) - -export const { login, logout } = userSlice.actions; -export const selectLoggedIn = (state) => state.user.loggedIn; -export const selectUser = (state) => state.user; - -export default userSlice.reducer \ No newline at end of file diff --git a/client/src/redux/slices/usersApiSlice.js b/client/src/redux/slices/usersApiSlice.js new file mode 100644 index 00000000..e3d8e7f4 --- /dev/null +++ b/client/src/redux/slices/usersApiSlice.js @@ -0,0 +1,31 @@ +import { apiSlice } from "./apiSlice"; + +const USERS_API = "/api/auth"; + +export const usersApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + login: builder.mutation({ + query: (credentials) => ({ + url: `${USERS_API}/login`, + method: "POST", + body: credentials, + }), + }), + logout: builder.mutation({ + query: () => ({ + url: `${USERS_API}/logout`, + method: "POST", + }), + }), + signup: builder.mutation({ + query: (data) => ({ + url: `${USERS_API}/signUp`, + method: "POST", + body: data, + }), + }), + }), +}); + +export const { useLoginMutation, useLogoutMutation, useSignupMutation } = + usersApi; diff --git a/client/src/redux/store.js b/client/src/redux/store.js index 8a57e88d..2b14e39c 100644 --- a/client/src/redux/store.js +++ b/client/src/redux/store.js @@ -1,13 +1,14 @@ -import { configureStore } from "@reduxjs/toolkit" -import userReducer from "./slices/userSlice" -import scoutsReducer from "./slices/scoutsSlice" - +import { configureStore } from "@reduxjs/toolkit"; +import authReducer from "./slices/authSlice.js"; +import { apiSlice } from "./slices/apiSlice.js"; const store = configureStore({ - reducer: { - user: userReducer, - scouts: scoutsReducer, - }, -}) + reducer: { + auth: authReducer, + [apiSlice.reducerPath]: apiSlice.reducer, + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware), + devTools: true, +}); -export default store; \ No newline at end of file +export default store; diff --git a/server/controllers/auth.controller.js b/server/controllers/auth.controller.js index 38de5dbf..569b443d 100644 --- a/server/controllers/auth.controller.js +++ b/server/controllers/auth.controller.js @@ -1,138 +1,139 @@ -import bcrypt from 'bcryptjs' -import db from '../database/db.js' -import { jsonToArray } from '../utils/convert.js' -import generateToken from '../utils/generateToken.js' +import bcrypt from "bcryptjs"; +import db from "../database/db.js"; +import { jsonToArray } from "../utils/convert.js"; +import generateToken from "../utils/generateToken.js"; const authController = { - - // @desc Create a new captain - // @route POST /api/auth/signup - // @access Public - signup: async (req, res) => { - try { - // get email and password from request body - const email = req.body['email'] - const password = req.body['password'] - - // Check if email already exists - const captain = await db.query( - `SELECT "email", "password" + // @desc Create a new captain + // @route POST /api/auth/signup + // @access Public + signup: async (req, res) => { + try { + // get email and password from request body + const email = req.body["email"]; + const password = req.body["password"]; + + // Check if email already exists + const captain = await db.query( + `SELECT "email", "password" FROM "Captain" WHERE "email" = $1;`, - [email] - ) - if (captain.rows.length) { - return res.status(400).json({ error: 'Email is taken!!' }) - } - - // Hash the password - const hashedPassword = await bcrypt.hash(password, 10) - - // Create a new Captain - req.body = { ...req.body, password: hashedPassword } - const params = jsonToArray(req.body) - const result = await db.query( - `INSERT INTO "Captain"("firstName", "middleName", "lastName", "phoneNumber", "email", "password", "gender", "type") + [email] + ); + if (captain.rows.length) { + return res.status(400).json({ error: "Email is taken!!" }); + } + + // Hash the password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create a new Captain + req.body = { ...req.body, password: hashedPassword }; + const params = jsonToArray(req.body); + const result = await db.query( + `INSERT INTO "Captain"("firstName", "middleName", "lastName", "phoneNumber", "email", "password", "gender", "type") VALUES($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *;`, - params.concat(['regular']) - ) - const newCaptain = result.rows[0] - - // Generate a JWT token - generateToken(res, newCaptain.captainId) - - // Send the response - res.status(201).json({ - message: 'Captain created successfully', - }) - } catch (error) { - console.log(error) - res.status(500).json({ - error: 'An error occurred while creating a new captain!!', - }) - } - }, - - // @desc Login a captain - // @route POST /api/auth/login - // @access Public - login: async (req, res) => { - try { - // Deconstruct the request body - const { email, password } = req.body - - // Check if email already exists - const result = await db.query( - `SELECT * + params.concat(["regular"]) + ); + const newCaptain = result.rows[0]; + + // Generate a JWT token + generateToken(res, newCaptain.captainId); + + // Send the response + res.status(201).json({ + message: "Captain created successfully", + data: newCaptain, + }); + } catch (error) { + console.log(error); + res.status(500).json({ + error: "An error occurred while creating a new captain!!", + }); + } + }, + + // @desc Login a captain + // @route POST /api/auth/login + // @access Public + login: async (req, res) => { + try { + // Deconstruct the request body + const { email, password } = req.body; + + // Check if email already exists + const result = await db.query( + `SELECT * FROM "Captain" WHERE "email" = $1;`, - [email] - ) - if (!result.rows.length) { - return res.status(400).json({ - error: 'Invalid email', - }) - } - - // Get Captain's data - const captain = result.rows[0] - - // Check if the password is correct - const isCorrect = await bcrypt.compare(password, captain.password) - if (!isCorrect) { - return res.status(400).json({ - error: 'Invalid password', - }) - } - - // Generate a JWT token - generateToken(res, captain.captainId) - - // Send the response - res.status(200).json({ - message: 'Logged in successfully', - }) - } catch (error) { - console.log(error) - res.status(500).json({ - error: 'An error occurred while logging you in', - }) - } - }, - - // @desc Logout a captain - // @route GET /api/auth/logout - // @access Private - logout: async (req, res) => { - try { - // Clear the cookie - res.clearCookie('token') - - // Send the response - res.status(200).json({ - message: 'Logged out successfully', - }) - } catch (error) { - console.log(error) - res.status(500).json({ - error: 'An error occurred while logging out', - }) - } - }, - - // @desc Auth logged-in captain - // @route GET /api/auth/me - // @access Private - me: (req, res) => { - try { - res.status(200).json({ user: req.captain }) - } catch (error) { - console.log(error) - res.status(500).json({ - error: 'An error occurred while fetching data.', - }) - } - }, -} - -export default authController + [email] + ); + if (!result.rows.length) { + return res.status(400).json({ + error: "Invalid email", + }); + } + + // Get Captain's data + const captain = result.rows[0]; + + // Check if the password is correct + const isCorrect = await bcrypt.compare(password, captain.password); + if (!isCorrect) { + return res.status(400).json({ + error: "Invalid password", + }); + } + + // Generate a JWT token + generateToken(res, captain.captainId); + + // Send the response + res.status(200).json({ + message: "Logged in successfully", + data: captain, + }); + } catch (error) { + console.log(error); + res.status(500).json({ + error: "An error occurred while logging you in", + }); + } + }, + + // @desc Logout a captain + // @route GET /api/auth/logout + // @access Private + logout: async (req, res) => { + try { + // Clear the cookie + res.clearCookie("token"); + + // Send the response + res.status(200).json({ + message: "Logged out successfully", + }); + } catch (error) { + console.log(error); + res.status(500).json({ + error: "An error occurred while logging out", + }); + } + }, + + // @desc Auth logged-in captain + // @route GET /api/auth/me + // @access Private + me: (req, res) => { + try { + res.status(200).json({ user: req.captain }); + } catch (error) { + console.log(error); + res.status(500).json({ + error: "An error occurred while fetching data.", + }); + } + }, +}; + +export default authController; diff --git a/server/database/fillDatabase.sql b/server/database/fillDatabase.sql new file mode 100644 index 00000000..3e735bb3 --- /dev/null +++ b/server/database/fillDatabase.sql @@ -0,0 +1,23 @@ +-- ADD CAPTAINS +INSERT INTO + "Captain"( + "firstName", + "middleName", + "lastName", + "phoneNumber", + "email", + "password", + "gender", + "type" + ) +VALUES + ( + "أمير", + "أنور", + "بخيت", + "01221461992", + "amir.kedis@gmail.com", + "$2a$10$82orQ3yruIoakCWUg/29KuXBwJlZiezJzxUW.8Ek.Jvc/MPLagDYS", + "male", + "regular" + ) RETURNING *; \ No newline at end of file