From a4e3dbdc0010ad467599b0ee9183e25c499886c4 Mon Sep 17 00:00:00 2001 From: Adam Kwiatkowski Date: Thu, 15 Jun 2023 03:42:30 +0200 Subject: [PATCH 1/3] Add user authorization --- .../SoftwareEngineering2/FlowerShopContext.cs | 9 +- frontend/client/package-lock.json | 152 ++++++++++++++++ frontend/client/package.json | 1 + frontend/client/src/context/AuthContext.tsx | 39 +++++ .../src/context/ShoppingCartContext.tsx | 129 ++++++++------ frontend/client/src/index.css | 4 + frontend/client/src/main.tsx | 37 +++- frontend/client/src/pages/LoginPage.tsx | 164 ++++++++++++++---- frontend/client/src/pages/LogoutPage.tsx | 12 ++ frontend/client/src/pages/ProductsPage.tsx | 9 +- frontend/client/src/pages/ProtectedRoute.tsx | 12 ++ 11 files changed, 473 insertions(+), 95 deletions(-) create mode 100644 frontend/client/src/context/AuthContext.tsx create mode 100644 frontend/client/src/pages/LogoutPage.tsx create mode 100644 frontend/client/src/pages/ProtectedRoute.tsx diff --git a/backend/SoftwareEngineering2/FlowerShopContext.cs b/backend/SoftwareEngineering2/FlowerShopContext.cs index 9212cc2..6610683 100644 --- a/backend/SoftwareEngineering2/FlowerShopContext.cs +++ b/backend/SoftwareEngineering2/FlowerShopContext.cs @@ -22,9 +22,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { // TODO: temporary solution (password: password) modelBuilder.Entity().HasData(new EmployeeModel { Email = "admin@flowershop.com", - Name = "W³adys³aw Howalski", + Name = "W�adys�aw Howalski", Password = "AQAAAAIAAYagAAAAEHiYiXUCLpBDCy3l60OqSPW+GNZExxF4PwXI8VtkhKZqjVsMFdhw68orF475JKPXkA==", EmployeeID = 1 }); + + modelBuilder.Entity().HasData(new ClientModel { + Email = "johndoe@example.com", + Name = "John Doe", + Password = "AQAAAAIAAYagAAAAEHiYiXUCLpBDCy3l60OqSPW+GNZExxF4PwXI8VtkhKZqjVsMFdhw68orF475JKPXkA==", + ClientID = 1 + }); } } \ No newline at end of file diff --git a/frontend/client/package-lock.json b/frontend/client/package-lock.json index 3f1d0b5..d65a481 100644 --- a/frontend/client/package-lock.json +++ b/frontend/client/package-lock.json @@ -13,6 +13,7 @@ "@headlessui/react": "^1.7.14", "@heroicons/react": "^2.0.17", "@mui/material": "^5.13.1", + "axios": "^1.4.0", "dotenv": "^16.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1380,6 +1381,11 @@ "node": "*" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -1425,6 +1431,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -1842,6 +1858,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1944,6 +1971,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2474,6 +2509,25 @@ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2483,6 +2537,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -3300,6 +3367,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4005,6 +4091,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6158,6 +6249,11 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -6178,6 +6274,16 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -6460,6 +6566,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -6533,6 +6647,11 @@ "clone": "^1.0.2" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6845,6 +6964,11 @@ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -6854,6 +6978,16 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -7433,6 +7567,19 @@ "picomatch": "^2.3.1" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -7917,6 +8064,11 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/frontend/client/package.json b/frontend/client/package.json index 3bd8f3c..fbc6b3e 100644 --- a/frontend/client/package.json +++ b/frontend/client/package.json @@ -14,6 +14,7 @@ "@headlessui/react": "^1.7.14", "@heroicons/react": "^2.0.17", "@mui/material": "^5.13.1", + "axios": "^1.4.0", "dotenv": "^16.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/client/src/context/AuthContext.tsx b/frontend/client/src/context/AuthContext.tsx new file mode 100644 index 0000000..1ed0bba --- /dev/null +++ b/frontend/client/src/context/AuthContext.tsx @@ -0,0 +1,39 @@ +import {createContext, ReactNode, useContext, useEffect, useMemo, useState} from "react"; +import axios from "axios"; + +type AuthContext = { + token: string | null, + setToken: (token: string | null) => void +} + +const AuthContext = createContext({} as AuthContext) + +export function useAuth() { + return useContext(AuthContext) +} + +export function AuthProvider({children}: { children: ReactNode }) { + const [token, setToken_] = useState(localStorage.getItem('token') ?? null) + + const setToken = (token: string | null) => { + setToken_(token) + } + + useEffect(() => { + if (token) { + axios.defaults.headers.common['Authorization'] = `Bearer ${token}` + localStorage.setItem('token', token) + } else { + delete axios.defaults.headers.common['Authorization'] + localStorage.removeItem('token') + } + }, [token]); + + const value = useMemo(() => ({token, setToken}), [token]) + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/frontend/client/src/context/ShoppingCartContext.tsx b/frontend/client/src/context/ShoppingCartContext.tsx index e69354c..c65842e 100644 --- a/frontend/client/src/context/ShoppingCartContext.tsx +++ b/frontend/client/src/context/ShoppingCartContext.tsx @@ -1,81 +1,110 @@ -import {createContext, ReactNode, useContext, useEffect, useState} from "react"; -import {CartItem} from "../models/CartItem"; +import { createContext, ReactNode, useContext, useEffect, useState } from "react"; +import { CartItem } from "../models/CartItem"; +import axios from "axios"; +import {debounce} from "@mui/material"; type ShoppingCartProviderProps = { - children: ReactNode -} + children: ReactNode; +}; type ShoppingCart = { - items: CartItem[], - addItem: (item: CartItem) => Promise, - updateItem: (item: CartItem) => Promise, - removeItem: (item: CartItem) => Promise, - clear: () => Promise, -} + items: CartItem[]; + addItem: (item: CartItem) => Promise; + updateItem: (item: CartItem) => Promise; + removeItem: (item: CartItem) => Promise; + clear: () => Promise; +}; -const ShoppingCartContext = createContext({} as ShoppingCart) +const ShoppingCartContext = createContext({} as ShoppingCart); export function useShoppingCart() { - return useContext(ShoppingCartContext) + return useContext(ShoppingCartContext); } +const updateItemDebounced = debounce(async (item: CartItem) => { + try { + await axios.put( + `${import.meta.env.VITE_API_BASE_URL}/api/basket/${item.product.productID}`, + item + ); + } catch (error) { + console.error(error); + } +}, 500); -export function ShoppingCartProvider({children}: ShoppingCartProviderProps) { - const [cartItems, setCartItems] = useState([]) +export function ShoppingCartProvider({ children }: ShoppingCartProviderProps) { + const [cartItems, setCartItems] = useState([]); useEffect(() => { async function fetchBasket() { - const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/basket`) - const items = await response.json() - setCartItems(items) + try { + const response = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/api/basket`); + const items = response.data; + setCartItems(items); + } catch (error) { + console.error(error); + } } - fetchBasket().catch((e) => console.error(e)) - }, []) + fetchBasket(); + }, []); async function addItem(item: CartItem) { - const existingItem = cartItems.find((i) => i.product.productID === item.product.productID) + const existingItem = cartItems.find((i) => i.product.productID === item.product.productID); if (existingItem) { - existingItem.quantity += item.quantity - await updateItem(existingItem) + existingItem.quantity += item.quantity; + await updateItem(existingItem); } else { - await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/basket`, { - method: 'POST', headers: { - 'Content-Type': 'application/json' - }, body: JSON.stringify(item) - }) - setCartItems([...cartItems, item]) + try { + await axios.post(`${import.meta.env.VITE_API_BASE_URL}/api/basket`, { + productID: item.product.productID, + quantity: item.quantity, + }); + setCartItems([...cartItems, item]); + } catch (error) { + console.error(error); + } } } async function updateItem(item: CartItem) { - await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/basket/${item.product.productID}`, { - method: 'PUT', headers: { - 'Content-Type': 'application/json' - }, body: JSON.stringify(item) - }) - setCartItems([...cartItems.map((i) => i.product.productID === item.product.productID ? item : i)]) + try { + await updateItemDebounced(item); + setCartItems([...cartItems.map((i) => (i.product.productID === item.product.productID ? item : i))]); + } catch (error) { + console.error(error); + } } async function removeItem(item: CartItem) { - await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/basket/${item.product.productID}`, { - method: 'DELETE' - }) - console.log(item) - console.log(cartItems) - setCartItems([...cartItems.filter((i) => i.product.productID !== item.product.productID)]) + try { + await axios.delete(`${import.meta.env.VITE_API_BASE_URL}/api/basket/${item.product.productID}`); + setCartItems([...cartItems.filter((i) => i.product.productID !== item.product.productID)]); + } catch (error) { + console.error(error); + } } async function clear() { - await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/basket`, { - method: 'DELETE' - }) - setCartItems([]) + try { + await axios.delete(`${import.meta.env.VITE_API_BASE_URL}/api/basket`); + setCartItems([]); + } catch (error) { + console.error(error); + } } - return - {children} - -} \ No newline at end of file + return ( + + {children} + + ); +} diff --git a/frontend/client/src/index.css b/frontend/client/src/index.css index a5a268e..c13377a 100644 --- a/frontend/client/src/index.css +++ b/frontend/client/src/index.css @@ -7,6 +7,10 @@ body { } @layer components { + .button-main { + @apply inline-flex items-center px-6 py-3 justify-center border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-neutral-950 hover:bg-neutral-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900; + } + input[type=range] { @apply absolute w-full pointer-events-none appearance-none h-full opacity-0 z-10 p-0; } diff --git a/frontend/client/src/main.tsx b/frontend/client/src/main.tsx index c3d9476..95ea5a5 100644 --- a/frontend/client/src/main.tsx +++ b/frontend/client/src/main.tsx @@ -4,10 +4,14 @@ import {createBrowserRouter, RouterProvider,} from "react-router-dom"; import './index.css' import Root from "./pages/Root"; import ErrorPage from "./pages/ErrorPage"; -import ProductsPage, {loader as productListLoader} from "./pages/ProductsPage"; +import ProductsPage from "./pages/ProductsPage"; import LoginPage from "./pages/LoginPage"; import ProductDetailsPage, {loader as productLoader} from "./pages/ProductDetailsPage"; import CartOverviewPage from "./pages/CartOverviewPage"; +// import CheckoutPage from "./pages/CheckoutPage"; +import {AuthProvider} from "./context/AuthContext"; +import {ProtectedRoute} from "./pages/ProtectedRoute"; +import LogoutPage from "./pages/LogoutPage"; const router = createBrowserRouter([ { @@ -17,12 +21,7 @@ const router = createBrowserRouter([ children: [ { path: "/", - element: , - loader: productListLoader, - }, - { - path: "login", - element: + element: }, { path: "products/:id", @@ -32,13 +31,33 @@ const router = createBrowserRouter([ { path: "cart", element: - } + }, + // { + // path: "checkout", + // element: + // } ] }, + { + path: "login", + element: + }, + { + path: "logout", + element: + }, + { + element: , + children: [ + + ] + } ]); ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - + + + ) diff --git a/frontend/client/src/pages/LoginPage.tsx b/frontend/client/src/pages/LoginPage.tsx index 7ba173e..fded582 100644 --- a/frontend/client/src/pages/LoginPage.tsx +++ b/frontend/client/src/pages/LoginPage.tsx @@ -1,47 +1,143 @@ -import {Link} from "react-router-dom"; -import React from "react"; +import { Link, useNavigate } from "react-router-dom"; +import React, { useState, useEffect } from "react"; +import { ArrowUturnLeftIcon } from "@heroicons/react/20/solid"; +import { useAuth } from "../context/AuthContext"; export default function LoginPage() { + const { token, setToken } = useAuth(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); // [1 + const navigate = useNavigate(); + + console.log(token) + + const handleLogin = () => { + // Send user credentials to /api/users/log_in + fetch(`${import.meta.env.VITE_API_BASE_URL}/api/users/log_in`, { + method: "POST", + body: JSON.stringify({ username, password }), + headers: { + "Content-Type": "application/json" + } + }) + .then(response => { + if (response.ok) { + return response.json(); + } else if (response.status === 401) { + setError(new Error("Invalid username or password")); + } else { + setError(new Error("An unknown error occurred")); + } + }) + .then(data => { + // Set the JWT token + setToken(data.jwttoken); + navigate("/"); + }) + .catch(error => { + // Show error prompt + console.error(error); + }); + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + handleLogin(); + } + }; + + useEffect(() => { + // Redirect the user when a token is set + if (token) { + navigate("/"); + } + }, [token]); return ( -
-
-

Welcome back!

-
- - - - - - -
-
- - +
+
+
+

Welcome back!

+
+ + setUsername(e.target.value)} + onKeyDown={handleKeyPress} + /> + + + setPassword(e.target.value)} + onKeyDown={handleKeyPress} + /> + +
+
+ + +
+ +

Forgot password?

+
- -

Forgot password?

-
+
- -
-

Login

-
- + {error && ( +
+ {error.message} +
+ )} + + -
-

Don't have an account?

- -

Register

- +
+

Don't have an account?

+ +

Register

+ +
+
+ Login +
+
+
+ + Back to the store +
); diff --git a/frontend/client/src/pages/LogoutPage.tsx b/frontend/client/src/pages/LogoutPage.tsx new file mode 100644 index 0000000..9c35182 --- /dev/null +++ b/frontend/client/src/pages/LogoutPage.tsx @@ -0,0 +1,12 @@ +import {useAuth} from "../context/AuthContext"; +import {useNavigate} from "react-router-dom"; + +export default function LogoutPage() { + const {setToken} = useAuth(); + const navigate = useNavigate(); + + setToken(null); + navigate('/', {replace: true}); + + return <>; +} \ No newline at end of file diff --git a/frontend/client/src/pages/ProductsPage.tsx b/frontend/client/src/pages/ProductsPage.tsx index ac55f2e..cbfb89e 100644 --- a/frontend/client/src/pages/ProductsPage.tsx +++ b/frontend/client/src/pages/ProductsPage.tsx @@ -43,7 +43,13 @@ export default function ProductsPage() { setLoading(true); setError(false); - const url = new URL(`${import.meta.env.VITE_API_BASE_URL}/api/products`); + let url: URL; + try { + url = new URL("/api/products", import.meta.env.VITE_API_BASE_URL); + } catch (e) { + url = new URL("/api/products", window.location.origin); + } + console.log(`URL: ${url}`) url.searchParams.append('pageNumber', page.toString()); url.searchParams.append('elementsOnPage', '10'); @@ -56,6 +62,7 @@ export default function ProductsPage() { setHasMore(data.length > 0); setLoading(false); }).catch((reason) => { + console.log(`Error while fetching products from ${url}`); console.error(reason); setError(true); setLoading(false); diff --git a/frontend/client/src/pages/ProtectedRoute.tsx b/frontend/client/src/pages/ProtectedRoute.tsx new file mode 100644 index 0000000..6eb2888 --- /dev/null +++ b/frontend/client/src/pages/ProtectedRoute.tsx @@ -0,0 +1,12 @@ +import {Navigate, Outlet} from "react-router-dom"; +import {useAuth} from "../context/AuthContext"; + +export const ProtectedRoute = () => { + const {token} = useAuth(); + + if (!token) { + return + } + + return +} \ No newline at end of file From 37c25b67e5b190d8e29dbdb7c7218bcf1e203a14 Mon Sep 17 00:00:00 2001 From: Adam Kwiatkowski Date: Thu, 15 Jun 2023 04:13:51 +0200 Subject: [PATCH 2/3] Add user popover --- .../client/src/components/UserPopover.tsx | 61 +++++++++++++++++++ frontend/client/src/context/AuthContext.tsx | 24 +++++--- frontend/client/src/models/Address.ts | 8 +++ frontend/client/src/models/User.ts | 10 +++ frontend/client/src/pages/LoginPage.tsx | 31 +++++----- frontend/client/src/pages/Root.tsx | 4 +- 6 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 frontend/client/src/components/UserPopover.tsx create mode 100644 frontend/client/src/models/Address.ts create mode 100644 frontend/client/src/models/User.ts diff --git a/frontend/client/src/components/UserPopover.tsx b/frontend/client/src/components/UserPopover.tsx new file mode 100644 index 0000000..0d97bfb --- /dev/null +++ b/frontend/client/src/components/UserPopover.tsx @@ -0,0 +1,61 @@ +import React, {Fragment} from 'react'; +import {Popover, Transition} from '@headlessui/react' +import {UserIcon} from "@heroicons/react/24/outline"; +import {useAuth} from "../context/AuthContext"; +import {Link} from "react-router-dom"; + +export default function UserPopover() { + const {user} = useAuth() + + return ( + + {({open}) => (<> + + + + + + +
+ {user ? (
+ Welcome, {user.name} + + + Logout + + +
) : (<> + + + Login + + + ) + } +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/client/src/context/AuthContext.tsx b/frontend/client/src/context/AuthContext.tsx index 1ed0bba..c6f1813 100644 --- a/frontend/client/src/context/AuthContext.tsx +++ b/frontend/client/src/context/AuthContext.tsx @@ -1,9 +1,9 @@ import {createContext, ReactNode, useContext, useEffect, useMemo, useState} from "react"; import axios from "axios"; +import {User} from "../models/User"; type AuthContext = { - token: string | null, - setToken: (token: string | null) => void + token: string | null, setToken: (token: string | null) => void, user: User | null, } const AuthContext = createContext({} as AuthContext) @@ -14,6 +14,7 @@ export function useAuth() { export function AuthProvider({children}: { children: ReactNode }) { const [token, setToken_] = useState(localStorage.getItem('token') ?? null) + const [user, setUser] = useState(null) const setToken = (token: string | null) => { setToken_(token) @@ -21,19 +22,24 @@ export function AuthProvider({children}: { children: ReactNode }) { useEffect(() => { if (token) { - axios.defaults.headers.common['Authorization'] = `Bearer ${token}` + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; localStorage.setItem('token', token) + + axios.get(`${import.meta.env.VITE_API_BASE_URL}/api/users`).then(res => { + console.log(res.data) + setUser(res.data) + }).catch(err => { + console.log(err) + }) } else { delete axios.defaults.headers.common['Authorization'] localStorage.removeItem('token') } }, [token]); - const value = useMemo(() => ({token, setToken}), [token]) + const value = useMemo(() => ({token, setToken, user}), [token, user]) - return ( - - {children} - - ); + return ( + {children} + ); } \ No newline at end of file diff --git a/frontend/client/src/models/Address.ts b/frontend/client/src/models/Address.ts new file mode 100644 index 0000000..76d1ef8 --- /dev/null +++ b/frontend/client/src/models/Address.ts @@ -0,0 +1,8 @@ +export type Address = { + street: string; + city: string; + buildingNo: string; + houseNo: string; + postalCode: string; + country: string; +} \ No newline at end of file diff --git a/frontend/client/src/models/User.ts b/frontend/client/src/models/User.ts new file mode 100644 index 0000000..24bf140 --- /dev/null +++ b/frontend/client/src/models/User.ts @@ -0,0 +1,10 @@ +import {Address} from "./Address"; + +export type User = { + userID: number; + name: string; + email: string; + role: string; + newsletter: boolean; + address: Address; +} \ No newline at end of file diff --git a/frontend/client/src/pages/LoginPage.tsx b/frontend/client/src/pages/LoginPage.tsx index fded582..2a91910 100644 --- a/frontend/client/src/pages/LoginPage.tsx +++ b/frontend/client/src/pages/LoginPage.tsx @@ -1,10 +1,10 @@ -import { Link, useNavigate } from "react-router-dom"; -import React, { useState, useEffect } from "react"; -import { ArrowUturnLeftIcon } from "@heroicons/react/20/solid"; -import { useAuth } from "../context/AuthContext"; +import {Link, useNavigate} from "react-router-dom"; +import React, {useEffect, useState} from "react"; +import {ArrowUturnLeftIcon} from "@heroicons/react/20/solid"; +import {useAuth} from "../context/AuthContext"; export default function LoginPage() { - const { token, setToken } = useAuth(); + const {token, setToken} = useAuth(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); // [1 @@ -16,7 +16,7 @@ export default function LoginPage() { // Send user credentials to /api/users/log_in fetch(`${import.meta.env.VITE_API_BASE_URL}/api/users/log_in`, { method: "POST", - body: JSON.stringify({ username, password }), + body: JSON.stringify({username, password}), headers: { "Content-Type": "application/json" } @@ -58,7 +58,8 @@ export default function LoginPage() {
-

Welcome back!

+

Welcome + back!

{error && ( -
+
{error.message}
)} - +

Don't have an account?

@@ -128,15 +130,16 @@ export default function LoginPage() { Login + alt="Login"/>
-
+
- Back to the store + Back to the store
diff --git a/frontend/client/src/pages/Root.tsx b/frontend/client/src/pages/Root.tsx index 9dcaced..f278849 100644 --- a/frontend/client/src/pages/Root.tsx +++ b/frontend/client/src/pages/Root.tsx @@ -1,8 +1,8 @@ -import {UserIcon} from "@heroicons/react/24/outline"; import {Link, Outlet} from "react-router-dom"; import {MagnifyingGlassIcon} from "@heroicons/react/24/solid"; import CartPopover from "../components/CartPopover"; import {ShoppingCartProvider} from "../context/ShoppingCartContext"; +import UserPopover from "../components/UserPopover"; export default function Root() { return ( @@ -22,7 +22,7 @@ export default function Root() { className="hidden sm:block w-full h-full form-input bg-transparent border-0 focus:ring-0 outline-none text-sm text-gray-700 placeholder-gray-500 pl-2"/>
- +
From 7eb25029b8790d01727cc516b899cc1afc50bee4 Mon Sep 17 00:00:00 2001 From: Adam Kwiatkowski Date: Thu, 15 Jun 2023 04:13:51 +0200 Subject: [PATCH 3/3] Add user popover --- .../client/src/components/UserPopover.tsx | 61 +++++++++++++++++++ frontend/client/src/context/AuthContext.tsx | 24 +++++--- .../src/context/ShoppingCartContext.tsx | 7 ++- frontend/client/src/main.tsx | 4 +- frontend/client/src/models/Address.ts | 8 +++ frontend/client/src/models/User.ts | 10 +++ frontend/client/src/pages/LoginPage.tsx | 31 +++++----- frontend/client/src/pages/Root.tsx | 4 +- 8 files changed, 118 insertions(+), 31 deletions(-) create mode 100644 frontend/client/src/components/UserPopover.tsx create mode 100644 frontend/client/src/models/Address.ts create mode 100644 frontend/client/src/models/User.ts diff --git a/frontend/client/src/components/UserPopover.tsx b/frontend/client/src/components/UserPopover.tsx new file mode 100644 index 0000000..0d97bfb --- /dev/null +++ b/frontend/client/src/components/UserPopover.tsx @@ -0,0 +1,61 @@ +import React, {Fragment} from 'react'; +import {Popover, Transition} from '@headlessui/react' +import {UserIcon} from "@heroicons/react/24/outline"; +import {useAuth} from "../context/AuthContext"; +import {Link} from "react-router-dom"; + +export default function UserPopover() { + const {user} = useAuth() + + return ( + + {({open}) => (<> + + + + + + +
+ {user ? (
+ Welcome, {user.name} + + + Logout + + +
) : (<> + + + Login + + + ) + } +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/client/src/context/AuthContext.tsx b/frontend/client/src/context/AuthContext.tsx index 1ed0bba..c6f1813 100644 --- a/frontend/client/src/context/AuthContext.tsx +++ b/frontend/client/src/context/AuthContext.tsx @@ -1,9 +1,9 @@ import {createContext, ReactNode, useContext, useEffect, useMemo, useState} from "react"; import axios from "axios"; +import {User} from "../models/User"; type AuthContext = { - token: string | null, - setToken: (token: string | null) => void + token: string | null, setToken: (token: string | null) => void, user: User | null, } const AuthContext = createContext({} as AuthContext) @@ -14,6 +14,7 @@ export function useAuth() { export function AuthProvider({children}: { children: ReactNode }) { const [token, setToken_] = useState(localStorage.getItem('token') ?? null) + const [user, setUser] = useState(null) const setToken = (token: string | null) => { setToken_(token) @@ -21,19 +22,24 @@ export function AuthProvider({children}: { children: ReactNode }) { useEffect(() => { if (token) { - axios.defaults.headers.common['Authorization'] = `Bearer ${token}` + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; localStorage.setItem('token', token) + + axios.get(`${import.meta.env.VITE_API_BASE_URL}/api/users`).then(res => { + console.log(res.data) + setUser(res.data) + }).catch(err => { + console.log(err) + }) } else { delete axios.defaults.headers.common['Authorization'] localStorage.removeItem('token') } }, [token]); - const value = useMemo(() => ({token, setToken}), [token]) + const value = useMemo(() => ({token, setToken, user}), [token, user]) - return ( - - {children} - - ); + return ( + {children} + ); } \ No newline at end of file diff --git a/frontend/client/src/context/ShoppingCartContext.tsx b/frontend/client/src/context/ShoppingCartContext.tsx index c65842e..a7035f4 100644 --- a/frontend/client/src/context/ShoppingCartContext.tsx +++ b/frontend/client/src/context/ShoppingCartContext.tsx @@ -1,5 +1,5 @@ -import { createContext, ReactNode, useContext, useEffect, useState } from "react"; -import { CartItem } from "../models/CartItem"; +import {createContext, ReactNode, useContext, useEffect, useState} from "react"; +import {CartItem} from "../models/CartItem"; import axios from "axios"; import {debounce} from "@mui/material"; @@ -20,6 +20,7 @@ const ShoppingCartContext = createContext({} as ShoppingCart); export function useShoppingCart() { return useContext(ShoppingCartContext); } + const updateItemDebounced = debounce(async (item: CartItem) => { try { await axios.put( @@ -32,7 +33,7 @@ const updateItemDebounced = debounce(async (item: CartItem) => { }, 500); -export function ShoppingCartProvider({ children }: ShoppingCartProviderProps) { +export function ShoppingCartProvider({children}: ShoppingCartProviderProps) { const [cartItems, setCartItems] = useState([]); useEffect(() => { diff --git a/frontend/client/src/main.tsx b/frontend/client/src/main.tsx index 95ea5a5..c6bba68 100644 --- a/frontend/client/src/main.tsx +++ b/frontend/client/src/main.tsx @@ -48,9 +48,7 @@ const router = createBrowserRouter([ }, { element: , - children: [ - - ] + children: [] } ]); diff --git a/frontend/client/src/models/Address.ts b/frontend/client/src/models/Address.ts new file mode 100644 index 0000000..76d1ef8 --- /dev/null +++ b/frontend/client/src/models/Address.ts @@ -0,0 +1,8 @@ +export type Address = { + street: string; + city: string; + buildingNo: string; + houseNo: string; + postalCode: string; + country: string; +} \ No newline at end of file diff --git a/frontend/client/src/models/User.ts b/frontend/client/src/models/User.ts new file mode 100644 index 0000000..24bf140 --- /dev/null +++ b/frontend/client/src/models/User.ts @@ -0,0 +1,10 @@ +import {Address} from "./Address"; + +export type User = { + userID: number; + name: string; + email: string; + role: string; + newsletter: boolean; + address: Address; +} \ No newline at end of file diff --git a/frontend/client/src/pages/LoginPage.tsx b/frontend/client/src/pages/LoginPage.tsx index fded582..2a91910 100644 --- a/frontend/client/src/pages/LoginPage.tsx +++ b/frontend/client/src/pages/LoginPage.tsx @@ -1,10 +1,10 @@ -import { Link, useNavigate } from "react-router-dom"; -import React, { useState, useEffect } from "react"; -import { ArrowUturnLeftIcon } from "@heroicons/react/20/solid"; -import { useAuth } from "../context/AuthContext"; +import {Link, useNavigate} from "react-router-dom"; +import React, {useEffect, useState} from "react"; +import {ArrowUturnLeftIcon} from "@heroicons/react/20/solid"; +import {useAuth} from "../context/AuthContext"; export default function LoginPage() { - const { token, setToken } = useAuth(); + const {token, setToken} = useAuth(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); // [1 @@ -16,7 +16,7 @@ export default function LoginPage() { // Send user credentials to /api/users/log_in fetch(`${import.meta.env.VITE_API_BASE_URL}/api/users/log_in`, { method: "POST", - body: JSON.stringify({ username, password }), + body: JSON.stringify({username, password}), headers: { "Content-Type": "application/json" } @@ -58,7 +58,8 @@ export default function LoginPage() {
-

Welcome back!

+

Welcome + back!

{error && ( -
+
{error.message}
)} - +

Don't have an account?

@@ -128,15 +130,16 @@ export default function LoginPage() { Login + alt="Login"/>
-
+
- Back to the store + Back to the store
diff --git a/frontend/client/src/pages/Root.tsx b/frontend/client/src/pages/Root.tsx index 9dcaced..f278849 100644 --- a/frontend/client/src/pages/Root.tsx +++ b/frontend/client/src/pages/Root.tsx @@ -1,8 +1,8 @@ -import {UserIcon} from "@heroicons/react/24/outline"; import {Link, Outlet} from "react-router-dom"; import {MagnifyingGlassIcon} from "@heroicons/react/24/solid"; import CartPopover from "../components/CartPopover"; import {ShoppingCartProvider} from "../context/ShoppingCartContext"; +import UserPopover from "../components/UserPopover"; export default function Root() { return ( @@ -22,7 +22,7 @@ export default function Root() { className="hidden sm:block w-full h-full form-input bg-transparent border-0 focus:ring-0 outline-none text-sm text-gray-700 placeholder-gray-500 pl-2"/>
- +