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 (
);
}
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