From b4fc1204a3cd771b95bc156462582481c246b030 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Tue, 24 Dec 2024 13:05:29 -0500 Subject: [PATCH 1/8] First attempt at token-based authentication on backend changes --- .../Controllers/GoogleAuthenticationController.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/API/app/Http/Controllers/GoogleAuthenticationController.php b/API/app/Http/Controllers/GoogleAuthenticationController.php index d915691..ddfd56a 100644 --- a/API/app/Http/Controllers/GoogleAuthenticationController.php +++ b/API/app/Http/Controllers/GoogleAuthenticationController.php @@ -68,6 +68,19 @@ public function handleLogin(Request $request): Response } //Auth::login($user); Auth::guard('web')->login($user); + // Determine the desired auth mode (cookie or token) + if ( $request->has('authMode') ) { + if ( $request->authMode === 'token' ) { + // Token-based authentication was requested by the user + // (THIS SHOULD ONLY BE USED IN THE FRONTEND FOR DEV/TESTING, NOT ON DEPLOYMENT!) + $token = $user->createToken('authToken')->plainTextToken; + + return response()->json([ + 'token' => $token, + 'status' => 'success' + ], 200); + } + } } else { Log::info("CAN'T LOGIN USER; IT'S NULL"); } From 8b87b8bbf979447fe09dbe093be5b6699de98b40 Mon Sep 17 00:00:00 2001 From: Cameron Green Date: Thu, 26 Dec 2024 20:07:59 -0500 Subject: [PATCH 2/8] Update backend for first checkpoint on usable frontend testing against deployed backend --- .../GoogleAuthenticationController.php | 10 ++++++--- ..._12_27_003011_update_tokenable_id_type.php | 21 +++++++++++++++++++ API/routes/web.php | 4 ++-- 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 API/database/migrations/2024_12_27_003011_update_tokenable_id_type.php diff --git a/API/app/Http/Controllers/GoogleAuthenticationController.php b/API/app/Http/Controllers/GoogleAuthenticationController.php index ddfd56a..9d096c9 100644 --- a/API/app/Http/Controllers/GoogleAuthenticationController.php +++ b/API/app/Http/Controllers/GoogleAuthenticationController.php @@ -58,7 +58,7 @@ public function handleLogin(Request $request): Response if ($googleId) { $request->merge(['user_id' => $googleId]); // Add user_id to request - Log::info("User validated, setting cookie in request and returning..."); + Log::info("User validated, setting cookie or token in request and returning..."); // This should work (consult official documentation for more) $user = User::where('user_id', $googleId)->first(); @@ -66,11 +66,11 @@ public function handleLogin(Request $request): Response if (DEBUG_MODE) { Log::debug("Logging in user: " . print_r($googleId, true)); } - //Auth::login($user); - Auth::guard('web')->login($user); + // Determine the desired auth mode (cookie or token) if ( $request->has('authMode') ) { if ( $request->authMode === 'token' ) { + Log::info('Token mode chosen'); // Token-based authentication was requested by the user // (THIS SHOULD ONLY BE USED IN THE FRONTEND FOR DEV/TESTING, NOT ON DEPLOYMENT!) $token = $user->createToken('authToken')->plainTextToken; @@ -81,10 +81,14 @@ public function handleLogin(Request $request): Response ], 200); } } + //Auth::login($user); + Auth::guard('web')->login($user); } else { Log::info("CAN'T LOGIN USER; IT'S NULL"); } + Log::info('Cookie mode chosen'); + // Allow original request to proceed return response()->json(['status' => 'success']); diff --git a/API/database/migrations/2024_12_27_003011_update_tokenable_id_type.php b/API/database/migrations/2024_12_27_003011_update_tokenable_id_type.php new file mode 100644 index 0000000..b74d24c --- /dev/null +++ b/API/database/migrations/2024_12_27_003011_update_tokenable_id_type.php @@ -0,0 +1,21 @@ +string('tokenable_id')->change(); + }); + } + + public function down(): void + { + Schema::table('personal_access_tokens', function (Blueprint $table) { + $table->integer('tokenable_id')->change(); + }); + } +}; diff --git a/API/routes/web.php b/API/routes/web.php index ab41f84..7be91a0 100644 --- a/API/routes/web.php +++ b/API/routes/web.php @@ -29,11 +29,11 @@ Route::post('/shopping-lists', [ShoppingListController::class, 'store']) ->middleware('auth:sanctum'); -// Route for retrieving all shopping list titles (used for Dashboard page) +// Route for retrieving all owned shopping list titles (used for Dashboard page) Route::get('/shopping-lists', [ShoppingListController::class, 'getUserShoppingLists']) ->middleware('auth:sanctum'); -// Route for retrieving all shopping list titles (used for Dashboard page) +// Route for retrieving all shared shopping list titles (used for Dashboard page) Route::get('/shopping-lists/shared', [ShoppingListController::class, 'getSharedShoppingLists']) ->middleware('auth:sanctum'); From 4cd5bcac9a550f2379c4028d5124455e231e10e3 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 26 Dec 2024 20:10:05 -0500 Subject: [PATCH 3/8] Update first checkpoint on usable issue #76 implementation with corresponding frontend changes --- Frontend/shared/.env | 3 +- Frontend/shared/generate-config.js | 1 + Frontend/shared/src/constants/BaseUrl.ts | 1 + .../createGroceryItem/createGroceryItem.ts | 27 +++++--- .../createShareLink/createShareLink.ts | 17 +++-- .../createSharingPermissions.ts | 17 +++-- .../createShoppingList/createShoppingList.ts | 33 +++++---- .../deleteGroceryItem/deleteGroceryItem.ts | 25 ++++--- .../deleteShoppingList/deleteShoppingList.ts | 16 +++-- .../fetchGroceryItems/fetchGroceryItems.ts | 16 +++-- .../fetchOwnedShoppingLists.ts | 17 +++-- .../fetchSharedShoppingLists.ts | 19 ++++-- .../fetchShoppingList/fetchShoppingList.ts | 37 +++++----- .../updateGroceryItem/updateGroceryItem.ts | 17 +++-- .../updateShoppingListTitle.ts | 26 ++++--- .../src/hooks/AuthContext/AuthContext.tsx | 67 +++++++++++-------- .../src/hooks/AuthContext/AuthContextType.ts | 1 + .../ShoppingListContext.tsx | 21 ++++-- .../src/pages/Dashboard/Dashboard.tsx | 12 ++-- .../ShoppingListDetailWithProvider.tsx | 5 +- .../ShoppingListShare/ShoppingListShare.tsx | 4 +- 21 files changed, 242 insertions(+), 140 deletions(-) diff --git a/Frontend/shared/.env b/Frontend/shared/.env index 2f01cf9..47d84dd 100644 --- a/Frontend/shared/.env +++ b/Frontend/shared/.env @@ -1,2 +1,3 @@ API_DOMAIN=api.speedcartapp.com -API_PORT=8443 \ No newline at end of file +API_PORT=8443 +TESTING_MODE=true \ No newline at end of file diff --git a/Frontend/shared/generate-config.js b/Frontend/shared/generate-config.js index 0632042..5f521ee 100644 --- a/Frontend/shared/generate-config.js +++ b/Frontend/shared/generate-config.js @@ -8,6 +8,7 @@ dotenv.config(); const config = { API_DOMAIN: process.env.API_DOMAIN, API_PORT: process.env.API_PORT, + TESTING_MODE: process.env.TESTING_MODE, }; // Write the config to a file in the shared directory diff --git a/Frontend/shared/src/constants/BaseUrl.ts b/Frontend/shared/src/constants/BaseUrl.ts index c1c0a0d..bfa45d4 100644 --- a/Frontend/shared/src/constants/BaseUrl.ts +++ b/Frontend/shared/src/constants/BaseUrl.ts @@ -6,3 +6,4 @@ const API_PORT = config.API_PORT; // This can be reused for all backend interactions export const BASE_URL: string = `https://${API_DOMAIN}:${API_PORT}`; +export const TESTING_MODE: boolean = config.TESTING_MODE === 'true'; \ No newline at end of file diff --git a/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts b/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts index fd287ea..e666cc6 100644 --- a/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts +++ b/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts @@ -1,13 +1,20 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; -export const createGroceryItem = async (item: GroceryItem) => { - return fetch(`${BASE_URL}/grocery-items`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify(item) - }); +export const createGroceryItem = async (authToken = '', item: GroceryItem) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } + + return fetch(`${BASE_URL}/grocery-items`, { + method: 'POST', + headers: headers, + credentials: 'include', + body: JSON.stringify(item) + }); }; \ No newline at end of file diff --git a/Frontend/shared/src/functions/createShareLink/createShareLink.ts b/Frontend/shared/src/functions/createShareLink/createShareLink.ts index f097aa5..f0da826 100644 --- a/Frontend/shared/src/functions/createShareLink/createShareLink.ts +++ b/Frontend/shared/src/functions/createShareLink/createShareLink.ts @@ -1,11 +1,18 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; + +export const createShareLink = async (authToken = '', shareListId: string, permissions: any) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } -export const createShareLink = async (shareListId: string, permissions: any) => { return fetch(`${BASE_URL}/share/${shareListId}`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, credentials: 'include', body: JSON.stringify( permissions diff --git a/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts b/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts index 7ce2526..ac6ce86 100644 --- a/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts +++ b/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts @@ -1,11 +1,18 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; + +export const createSharingPermissions = async (authToken = '', token: string) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } -export const createSharingPermissions = async (token: string) => { return fetch(`${BASE_URL}/share/${token}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, credentials: 'include' }); } \ No newline at end of file diff --git a/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts b/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts index e060fb6..68a46ae 100644 --- a/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts +++ b/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts @@ -1,15 +1,22 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; -export const createShoppingList = async (name: string, routeId: any = null) => { - return fetch(`${BASE_URL}/shopping-lists`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ - name: name, - route_id: routeId - }) - }); +export const createShoppingList = async (authToken = '', name: string, routeId: any = null) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } + + return fetch(`${BASE_URL}/shopping-lists`, { + method: 'POST', + headers: headers, + credentials: 'include', + body: JSON.stringify({ + name: name, + route_id: routeId + }) + }); }; \ No newline at end of file diff --git a/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts b/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts index eefd6b5..7f9e9aa 100644 --- a/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts +++ b/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts @@ -1,12 +1,19 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; -export const deleteGroceryItem = async (item: GroceryItem) => { - fetch(`${BASE_URL}/grocery-items/${item.item_id}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - }); +export const deleteGroceryItem = async (authToken = '', item: GroceryItem) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } + + fetch(`${BASE_URL}/grocery-items/${item.item_id}`, { + method: 'DELETE', + headers: headers, + credentials: 'include', + }); } \ No newline at end of file diff --git a/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts b/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts index 1076c88..b13fe3d 100644 --- a/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts +++ b/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts @@ -1,11 +1,17 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; -export const deleteShoppingList = async (listId: string) => { +export const deleteShoppingList = async (authToken = '', listId: string) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } return fetch(`${BASE_URL}/shopping-lists/${listId}`, { method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, credentials: 'include' // Include cookies in the request }) }; \ No newline at end of file diff --git a/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts b/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts index 87d7d03..9d6a706 100644 --- a/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts +++ b/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts @@ -1,12 +1,18 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; -export const fetchGroceryItems = async (listId: string) => { +export const fetchGroceryItems = async (authToken = '', listId: string) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } return fetch(`${BASE_URL}/grocery-items/${listId}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, credentials: 'include', }); }; diff --git a/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts b/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts index dc1baba..2280680 100644 --- a/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts +++ b/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts @@ -1,11 +1,18 @@ import { BASE_URL } from '@constants'; -export const fetchOwnedShoppingLists = () => { +export const fetchOwnedShoppingLists = (authToken = '') => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } + return fetch(`${BASE_URL}/shopping-lists`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include' // Include cookies in the request + headers: headers, + credentials: 'include' }); }; \ No newline at end of file diff --git a/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts b/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts index 4eac7c5..6b7903e 100644 --- a/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts +++ b/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts @@ -1,11 +1,18 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; + +export const fetchSharedShoppingLists = (authToken = '') => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } -export const fetchSharedShoppingLists = () => { return fetch(`${BASE_URL}/shopping-lists/shared`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include' // Include cookies in the request + headers: headers, + credentials: 'include' }); }; \ No newline at end of file diff --git a/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts b/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts index 8610659..b7de45c 100644 --- a/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts +++ b/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts @@ -1,21 +1,26 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; -export const fetchShoppingList = async (listId: string) => { - const url = `${BASE_URL}/shopping-lists/${listId}`; +export const fetchShoppingList = async (authToken = '', listId: string) => { + const url = `${BASE_URL}/shopping-lists/${listId}`; + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - // Add any authorization headers if needed - }, - credentials: "include" - }); + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } - if (!response.ok) { - throw new Error(`Failed to fetch shopping list with ID ${listId}`); - } + const response = await fetch(url, { + method: 'GET', + headers: headers, + credentials: "include" + }); - // Return JSON response - return response.json(); + if (!response.ok) { + throw new Error(`Failed to fetch shopping list with ID ${listId}`); + } + + // Return JSON response + return response.json(); }; \ No newline at end of file diff --git a/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts b/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts index 66337ff..38c86b4 100644 --- a/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts +++ b/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts @@ -1,12 +1,19 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; -export const updateGroceryItem = async (item: GroceryItem) => { +export const updateGroceryItem = async (authToken = '', item: GroceryItem) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } + return fetch(`${BASE_URL}/grocery-items/${item.item_id}`, { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, credentials: 'include', body: JSON.stringify(item) }); diff --git a/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts b/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts index 701d737..20561a9 100644 --- a/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts +++ b/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts @@ -1,12 +1,18 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; -export const updateShoppingListTitle = async (shoppingListName: string, shoppingListId: string) => { - return fetch(`${BASE_URL}/shopping-lists/${shoppingListId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ name: shoppingListName }) - }); +export const updateShoppingListTitle = async (authToken = '', shoppingListName: string, shoppingListId: string) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } + return fetch(`${BASE_URL}/shopping-lists/${shoppingListId}`, { + method: 'PUT', + headers: headers, + credentials: 'include', + body: JSON.stringify({ name: shoppingListName }) + }); } \ No newline at end of file diff --git a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx index d60eeb7..0d02ee7 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx +++ b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx @@ -1,9 +1,9 @@ -import React, { createContext, useContext, useState, useEffect } from "react"; -import { jwtDecode } from "jwt-decode"; -import { googleLogout } from "@react-oauth/google"; +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { jwtDecode } from 'jwt-decode'; +import { googleLogout } from '@react-oauth/google'; import { BASE_URL } from '@constants'; -import { AuthContextType } from "./AuthContextType"; -import { GoogleToken } from "@types"; +import { AuthContextType } from './AuthContextType'; +import { GoogleToken } from '@types'; // Initialize the context with a default value of `null` const AuthContext = createContext(null); @@ -11,13 +11,15 @@ const AuthContext = createContext(null); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [userPictureLink, setUserPictureLink] = useState(null); + const [authToken, setAuthToken] = useState(''); const [loading, setLoading] = useState(true); // State for tracking loading of other context states (necessary for page loads) useEffect(() => { - const token = localStorage.getItem("speedcart_auth_exists"); - if (token === "true") { + const token = localStorage.getItem('speedcart_auth_exists'); + if (token === 'true') { setIsAuthenticated(true); - setUserPictureLink(localStorage.getItem("userImageUrl")); + setAuthToken(localStorage.getItem('speedcart_auth_token')); + setUserPictureLink(localStorage.getItem('userImageUrl')); } else { setIsAuthenticated(false); } @@ -29,43 +31,50 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // Initialize CSRF protection for the application fetch(`${BASE_URL}/sanctum/csrf-cookie`, { - method: "GET", - credentials: "include", // Important: include credentials to allow the cookie to be set + method: 'GET', + credentials: 'include', // Important: include credentials to allow the cookie to be set headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', 'Accept': 'application/json', }, }).then(csrfResponse => { console.log(`Response for CSRF token: ${csrfResponse.status} ${csrfResponse.statusText} and ${csrfResponse.type}`);// Get the CSRF token from cookies (document.cookies) - //const csrfToken = getCookie("XSRF-TOKEN"); + //const csrfToken = getCookie('XSRF-TOKEN'); //console.log(`csrf token retrieved: ${csrfToken}`); // Verify Google JWT with your backend fetch(`${BASE_URL}/auth/google`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, 'Accept': 'application/json', }, - credentials: "include", + credentials: 'include', + body: JSON.stringify({ + authMode: 'token', // Specify the auth mode here + }) }).then((response) => { //console.log(`Login response: ${JSON.stringify(response)}`); if (response.status === 200) { setIsAuthenticated(true); - localStorage.setItem("speedcart_auth_bearer_token", token); - localStorage.setItem("speedcart_auth_exists", 'true'); - localStorage.setItem("userImageUrl", userInfo.picture); + localStorage.setItem('speedcart_auth_bearer_token', token); + localStorage.setItem('speedcart_auth_exists', 'true'); + localStorage.setItem('userImageUrl', userInfo.picture); } return response.json(); }) .then((data) => { // Handle the response data here - console.log("Response text: " + JSON.stringify(data) + " and data token: " + JSON.stringify(data.token)); - localStorage.setItem("speedcart_auth_bearer_token", JSON.stringify(data.token)); + console.log('Response text: ' + JSON.stringify(data) + ' and data token: ' + JSON.stringify(data.token)); + localStorage.setItem('speedcart_auth_bearer_token', JSON.stringify(data.token)); + // Only needed for testing + localStorage.setItem('speedcart_auth_token', data.token); + console.log("Data token retrieved: " + localStorage.getItem('speedcart_auth_token')); + setAuthToken(data.token); }) .catch((error) => { // Handle errors here - console.error("Error:", error); + console.error('Error:', error); }); setIsAuthenticated(true); setUserPictureLink(userInfo.picture); @@ -79,33 +88,33 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children googleLogout(); fetch(`${BASE_URL}/auth/google`, { - method: "DELETE", - credentials: "include", // Include cookies in the request + method: 'DELETE', + credentials: 'include', // Include cookies in the request headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', 'Accept': 'application/json', }, // No Bearer token necessary (cookie contains JWT that will be deleted on backend) }) .then(() => { - localStorage.removeItem("speedcart_auth_exists"); - localStorage.removeItem("userImageUrl"); + localStorage.removeItem('speedcart_auth_exists'); + localStorage.removeItem('userImageUrl'); setIsAuthenticated(false); setUserPictureLink(null); }) .catch((error) => { - console.error("Error:", error); + console.error('Error:', error); }); }; - return {children}; + return {children}; }; // Create a custom hook to use the AuthContext export const useAuth = (): AuthContextType => { const context: AuthContextType = useContext(AuthContext); if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); + throw new Error('useAuth must be used within an AuthProvider'); } return context; }; \ No newline at end of file diff --git a/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts b/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts index 6d622c6..4e02a0a 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts +++ b/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts @@ -2,6 +2,7 @@ export interface AuthContextType { isAuthenticated: boolean; loading: boolean; + authToken: string; userPictureLink: string | null; login: (token: string) => void; logout: () => void; diff --git a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx index d4aab68..3be35b2 100644 --- a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx +++ b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx @@ -9,6 +9,8 @@ const ShoppingListContext = createContext(null); // This component handles all state-related work for any pages // that deal with saving shopping lists export const ShoppingListProvider = ({ children }) => { + // THIS ONE IS SPECIFICALLY ONLY NEEDED FOR TESTING + const [authToken, setAuthToken] = useState(''); const [shoppingListIDIsLoading, setShoppingListIDIsLoading] = useState(true); const [listTitle, setListTitle] = useState(''); const [listID, setListID] = useState(null); @@ -29,6 +31,12 @@ export const ShoppingListProvider = ({ children }) => { } }, [listID]); + useEffect(() => { + // Upon mount, we should be able to expect an authToken stored in localStorage + // (since none of the code in this file would even work if the user wasn't authenticated) + setAuthToken(localStorage.getItem('speedcart_auth_token')); + }, []); + // Handlers for all changes to state const handleNewItemChange = (index, newItem) => { const newItemsTemp = [...newItems]; @@ -98,7 +106,7 @@ export const ShoppingListProvider = ({ children }) => { if (crudMode === CrudMode.CREATE) { // We're creating a new list - const shoppingListResponse = await createShoppingList(listTitle); // Create the shopping list + const shoppingListResponse = await createShoppingList(authToken, listTitle); // Create the shopping list if (!shoppingListResponse.ok) { throw new Error(`HTTP error! status: ${shoppingListResponse.status}`); } @@ -110,7 +118,7 @@ export const ShoppingListProvider = ({ children }) => { } else { // We're updating an existing list // Update shopping list title - const listResponse = await updateShoppingListTitle(shoppingList.name, listID.toString()); + const listResponse = await updateShoppingListTitle(authToken, shoppingList.name, listID.toString()); if (!listResponse.ok) { throw new Error('Failed to update shopping list title'); @@ -120,21 +128,21 @@ export const ShoppingListProvider = ({ children }) => { if (existingItems.length > 0) { // Update each existing grocery item - const itemPromises = existingItems.map(item =>updateGroceryItem(item)); + const itemPromises = existingItems.map(item =>updateGroceryItem(authToken, item)); await Promise.all(itemPromises); } if (deletedItems.length > 0) { // Remove each grocery item that the user wants to delete - const itemDeletePromises = deletedItems.map(item => deleteGroceryItem(item)); + const itemDeletePromises = deletedItems.map(item => deleteGroceryItem(authToken, item)); await Promise.all(itemDeletePromises); } if (newItems.length > 0) { // Add each new item the user wants to add - const itemCreationPromises = newItems.map(item => createGroceryItem({ ...item, shopping_list_id: currentListID })); + const itemCreationPromises = newItems.map(item => createGroceryItem(authToken, { ...item, shopping_list_id: currentListID })); await Promise.all(itemCreationPromises); } @@ -142,7 +150,8 @@ export const ShoppingListProvider = ({ children }) => { }; return ( - (false); const [canDelete, setCanDelete] = useState(false); - const { isAuthenticated, logout }: AuthContextType = useAuth(); + const { isAuthenticated, authToken, logout }: AuthContextType = useAuth(); useEffect(() => { document.title = "View shopping lists"; @@ -43,9 +43,9 @@ function Dashboard() { setSharedListsAreLoading(true); setSharedListsError(null); setError(null); - console.log('Starting queries...'); + console.log('Starting queries with authToken: ' + authToken); // Retrieve lists owned by user - fetchOwnedShoppingLists() + fetchOwnedShoppingLists(authToken) .then(response => { console.log('Owned lists response:', response); // Log the response object if (!response.ok) { @@ -69,7 +69,7 @@ function Dashboard() { }); // Retrieve lists shared with user - fetchSharedShoppingLists() + fetchSharedShoppingLists(authToken) .then(response => { if (!response.ok) { console.log('Shared lists response:', response); // Log the response object @@ -118,7 +118,7 @@ function Dashboard() { return; } - deleteShoppingList(listId) + deleteShoppingList(authToken, listId) .then(response => { if (!response.ok) { if (response.status === 401) { @@ -150,7 +150,7 @@ function Dashboard() { try { setShareLink('Generating link...'); - const response = await createShareLink(shareListId, { + const response = await createShareLink(authToken, shareListId, { can_update: canUpdate, can_delete: canDelete }); diff --git a/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx b/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx index 4f4b49d..44f6f48 100644 --- a/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx +++ b/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx @@ -43,7 +43,8 @@ const ShoppingListDetail = () => { useEffect(() => { const fetchData = async () => { try { - const listData = await fetchShoppingList(id); + const authToken = localStorage.getItem('speedcart_auth_token'); + const listData = await fetchShoppingList(authToken, id); setShoppingList(listData); setListID(id); @@ -52,7 +53,7 @@ const ShoppingListDetail = () => { document.title = `Viewing list: ${listData.name}`; - const itemsDataResponse = await fetchGroceryItems(id); + const itemsDataResponse = await fetchGroceryItems(authToken, id); if (!itemsDataResponse.ok) { throw new Error(`Failed to fetch grocery items for shopping list with ID ${id}`); } diff --git a/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx b/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx index 4f617e6..a3fa030 100644 --- a/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx +++ b/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx @@ -13,7 +13,7 @@ function ShoppingListShare() { const navigate = useNavigate(); const [shareInteractionStatus, setShareInteractionStatus] = useState("Loading..."); const { token } = useParams(); // Get link sharing token from url parameters - const { isAuthenticated, login } = useAuth(); + const { authToken, isAuthenticated, login } = useAuth(); useEffect(() => { // Only attempt to verify the share interaction if there's a token and the user is authenticated @@ -54,7 +54,7 @@ function ShoppingListShare() { const verifyShareInteraction = async (token) => { try { - const response = await createSharingPermissions(token); + const response = await createSharingPermissions(authToken, token); //const responseText = await response.text(); //console.log("Response text:", responseText); From c0f94bcef56e87a46012feb6570334a3e9fe3896 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 11 Jan 2025 09:45:49 -0500 Subject: [PATCH 4/8] Add some basic improvements to tool documentation --- docs/dev/Tools.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/dev/Tools.md b/docs/dev/Tools.md index 89f9a2b..68b8e39 100644 --- a/docs/dev/Tools.md +++ b/docs/dev/Tools.md @@ -5,4 +5,9 @@ used in the development of SpeedCart - React and NPM - Purpose: Primary front-end development tools - PHP - - Purpose: Primary back-end programming language \ No newline at end of file + - Purpose: Primary back-end programming language +- Laravel + - Purpose: Framework for back-end PHP code, simplifies and streamlines + development through abstraction and widespread support +- Docker + - Purpose: Containerize back-end for portability and scalability \ No newline at end of file From 47690c2f82b3fa87b4c22615504fff99c084a15a Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 11 Jan 2025 09:46:24 -0500 Subject: [PATCH 5/8] First usable checkpoint for new callBackendAPI context refactor --- .../backendAuthFetch/backendAuthFetch.ts | 33 ++++++++++++++++ .../src/functions/backendAuthFetch/index.ts | 1 + .../createGroceryItem/createGroceryItem.ts | 30 +++++++------- .../createShareLink/createShareLink.ts | 39 +++++++++---------- .../createShoppingList/createShoppingList.ts | 37 ++++++++---------- .../src/hooks/AuthContext/AuthContext.tsx | 14 ++++++- .../src/hooks/AuthContext/AuthContextType.ts | 5 +++ Frontend/shared/src/types/BackendFunction.ts | 1 + Frontend/shared/src/types/index.d.ts | 3 +- Frontend/shared/tsconfig.json | 3 +- .../ShoppingListContext.tsx | 9 ++++- .../src/pages/Dashboard/Dashboard.tsx | 5 ++- 12 files changed, 116 insertions(+), 64 deletions(-) create mode 100644 Frontend/shared/src/functions/backendAuthFetch/backendAuthFetch.ts create mode 100644 Frontend/shared/src/functions/backendAuthFetch/index.ts create mode 100644 Frontend/shared/src/types/BackendFunction.ts diff --git a/Frontend/shared/src/functions/backendAuthFetch/backendAuthFetch.ts b/Frontend/shared/src/functions/backendAuthFetch/backendAuthFetch.ts new file mode 100644 index 0000000..e7810a6 --- /dev/null +++ b/Frontend/shared/src/functions/backendAuthFetch/backendAuthFetch.ts @@ -0,0 +1,33 @@ +import { BASE_URL, TESTING_MODE } from '@constants'; + +/** + * A utility function for making authenticated API requests to the backend. + * Automatically includes credentials and Authorization headers when needed. + */ +export const backendAuthFetch = async (endpoint: string, options: RequestInit = {}, authToken = '') => { + const headersInit: HeadersInit = { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...options.headers, + }; + + const headers: Headers = new Headers(headersInit); + + if (TESTING_MODE && authToken !== '') { + headers.set("Authorization", `Bearer ${authToken}`); + } + + const fetchOptions: RequestInit = { + ...options, + headers, + credentials: 'include', + }; + + const response = await fetch(`${BASE_URL}${endpoint}`, fetchOptions); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response; +}; \ No newline at end of file diff --git a/Frontend/shared/src/functions/backendAuthFetch/index.ts b/Frontend/shared/src/functions/backendAuthFetch/index.ts new file mode 100644 index 0000000..e277ecc --- /dev/null +++ b/Frontend/shared/src/functions/backendAuthFetch/index.ts @@ -0,0 +1 @@ +export { backendAuthFetch } from './backendAuthFetch'; \ No newline at end of file diff --git a/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts b/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts index e666cc6..30f1cdc 100644 --- a/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts +++ b/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts @@ -1,20 +1,18 @@ -import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; +import { backendAuthFetch } from "../backendAuthFetch"; +import { BackendFunction } from "@types"; -export const createGroceryItem = async (authToken = '', item: GroceryItem) => { - const headers: any = { - 'Content-Type': 'application/json', - "Accept" : "application/json" - }; +export const createGroceryItem: BackendFunction< + { item: GroceryItem }, + Response +> = async (authToken = '', { item }) => { - if (TESTING_MODE && authToken !== '') { - headers['Authorization'] = `Bearer ${authToken}`; - } - - return fetch(`${BASE_URL}/grocery-items`, { - method: 'POST', - headers: headers, - credentials: 'include', - body: JSON.stringify(item) - }); + return backendAuthFetch( + `/grocery-items`, + { + method: 'POST', + body: JSON.stringify(item), + }, + authToken + ); }; \ No newline at end of file diff --git a/Frontend/shared/src/functions/createShareLink/createShareLink.ts b/Frontend/shared/src/functions/createShareLink/createShareLink.ts index f0da826..91f5d13 100644 --- a/Frontend/shared/src/functions/createShareLink/createShareLink.ts +++ b/Frontend/shared/src/functions/createShareLink/createShareLink.ts @@ -1,21 +1,20 @@ -import { BASE_URL, TESTING_MODE } from '@constants'; +import { backendAuthFetch } from "../backendAuthFetch"; +import { BackendFunction } from "@types"; -export const createShareLink = async (authToken = '', shareListId: string, permissions: any) => { - const headers: any = { - 'Content-Type': 'application/json', - "Accept" : "application/json" - }; - - if (TESTING_MODE && authToken !== '') { - headers['Authorization'] = `Bearer ${authToken}`; - } - - return fetch(`${BASE_URL}/share/${shareListId}`, { - method: 'POST', - headers: headers, - credentials: 'include', - body: JSON.stringify( - permissions - ), - }); -} \ No newline at end of file +export const createShareLink: BackendFunction< +{ shareListId: string; permissions: string }, +Response +> = async ( + authToken = '', + {shareListId, + permissions} +) => { + return backendAuthFetch( + `/share/${shareListId}`, + { + method: 'POST', + body: JSON.stringify(permissions), + }, + authToken + ); +}; diff --git a/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts b/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts index 68a46ae..af10986 100644 --- a/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts +++ b/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts @@ -1,22 +1,19 @@ -import { BASE_URL, TESTING_MODE } from '@constants'; +import { backendAuthFetch } from "../backendAuthFetch"; +import { BackendFunction } from "@types"; -export const createShoppingList = async (authToken = '', name: string, routeId: any = null) => { - const headers: any = { - 'Content-Type': 'application/json', - "Accept" : "application/json" - }; - - if (TESTING_MODE && authToken !== '') { - headers['Authorization'] = `Bearer ${authToken}`; - } - - return fetch(`${BASE_URL}/shopping-lists`, { - method: 'POST', - headers: headers, - credentials: 'include', - body: JSON.stringify({ - name: name, - route_id: routeId - }) - }); +export const createShoppingList: BackendFunction< + { name: string; routeId: any }, + Response +> = async (authToken = '', { name, routeId }) => { + return backendAuthFetch( + `/shopping-lists`, + { + method: 'POST', + body: JSON.stringify({ + name: name, + route_id: routeId + }), + }, + authToken + ); }; \ No newline at end of file diff --git a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx index 0d02ee7..2a8ad76 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx +++ b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx @@ -3,7 +3,7 @@ import { jwtDecode } from 'jwt-decode'; import { googleLogout } from '@react-oauth/google'; import { BASE_URL } from '@constants'; import { AuthContextType } from './AuthContextType'; -import { GoogleToken } from '@types'; +import { GoogleToken, BackendFunction } from '@types'; // Initialize the context with a default value of `null` const AuthContext = createContext(null); @@ -107,7 +107,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }); }; - return {children}; + // Something like this, but we'd give stricter types and all share functions + // would fit an interface for their input shape to normalize them all + const callBackendAPI = async ( + endpointFunc: BackendFunction, + args: TArgs + ): Promise => { + // authToken is already managed by AuthProvider + return endpointFunc(authToken, args); + }; + + return {children}; }; // Create a custom hook to use the AuthContext diff --git a/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts b/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts index 4e02a0a..cb58be2 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts +++ b/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts @@ -6,4 +6,9 @@ export interface AuthContextType { userPictureLink: string | null; login: (token: string) => void; logout: () => void; + // Adding the callBackendAPI function + callBackendAPI: ( + endpointFunc: (authToken: string, args: TArgs) => Promise, + args: TArgs + ) => Promise; } \ No newline at end of file diff --git a/Frontend/shared/src/types/BackendFunction.ts b/Frontend/shared/src/types/BackendFunction.ts new file mode 100644 index 0000000..80cd6b0 --- /dev/null +++ b/Frontend/shared/src/types/BackendFunction.ts @@ -0,0 +1 @@ +export type BackendFunction = (authToken: string, args: TArgs) => Promise; diff --git a/Frontend/shared/src/types/index.d.ts b/Frontend/shared/src/types/index.d.ts index 2df16aa..735b553 100644 --- a/Frontend/shared/src/types/index.d.ts +++ b/Frontend/shared/src/types/index.d.ts @@ -1,3 +1,4 @@ // src/types/index.d.ts export * from "./GroceryItem"; -export * from "./GoogleToken"; \ No newline at end of file +export * from "./GoogleToken"; +export * from "./BackendFunction"; \ No newline at end of file diff --git a/Frontend/shared/tsconfig.json b/Frontend/shared/tsconfig.json index b1d6781..5295879 100644 --- a/Frontend/shared/tsconfig.json +++ b/Frontend/shared/tsconfig.json @@ -4,7 +4,8 @@ "baseUrl": "./", // Base directory for resolving non-relative module names "paths": { "@constants": ["src/constants"], - "@types": ["src/types"] + "@types": ["src/types"], + "@hooks": ["src/hooks"] }, "module": "commonjs", "lib": ["es6", "dom"], diff --git a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx index 3be35b2..32b03b4 100644 --- a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx +++ b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx @@ -2,13 +2,14 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; // Import uuid library for unique item identification import { CrudMode } from '@constants/crudmodes'; -import { createGroceryItem, createShoppingList, deleteGroceryItem, updateGroceryItem, updateShoppingListTitle } from 'shared'; +import { AuthContextType, createGroceryItem, createShoppingList, deleteGroceryItem, updateGroceryItem, updateShoppingListTitle, useAuth } from 'shared'; const ShoppingListContext = createContext(null); // This component handles all state-related work for any pages // that deal with saving shopping lists export const ShoppingListProvider = ({ children }) => { + const { callBackendAPI }: AuthContextType = useAuth(); // THIS ONE IS SPECIFICALLY ONLY NEEDED FOR TESTING const [authToken, setAuthToken] = useState(''); const [shoppingListIDIsLoading, setShoppingListIDIsLoading] = useState(true); @@ -106,7 +107,11 @@ export const ShoppingListProvider = ({ children }) => { if (crudMode === CrudMode.CREATE) { // We're creating a new list - const shoppingListResponse = await createShoppingList(authToken, listTitle); // Create the shopping list + const shoppingListResponse: Response = await callBackendAPI(createShoppingList, { + name: listTitle, + route_id: null, + }); + if (!shoppingListResponse.ok) { throw new Error(`HTTP error! status: ${shoppingListResponse.status}`); } diff --git a/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx b/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx index 3de098d..d5c6778 100644 --- a/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx +++ b/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx @@ -27,7 +27,7 @@ function Dashboard() { const [shareLink, setShareLink] = useState('Link will show here'); const [canUpdate, setCanUpdate] = useState(false); const [canDelete, setCanDelete] = useState(false); - const { isAuthenticated, authToken, logout }: AuthContextType = useAuth(); + const { isAuthenticated, authToken, logout, callBackendAPI }: AuthContextType = useAuth(); useEffect(() => { document.title = "View shopping lists"; @@ -150,7 +150,8 @@ function Dashboard() { try { setShareLink('Generating link...'); - const response = await createShareLink(authToken, shareListId, { + const response: Response = await callBackendAPI(createShareLink, { + shareListId: shareListId, can_update: canUpdate, can_delete: canDelete }); From 8f3dd6f1bc478949cb95a74704ea0071b0f54357 Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 11 Jan 2025 09:46:51 -0500 Subject: [PATCH 6/8] First changes for local backend testing/running --- API/docker/development/docker-compose.yml | 29 +++++++++++ API/docker/{ => development}/nginx/Dockerfile | 0 .../development/nginx/conf.d/default.conf | 23 +++++++++ .../nginx/snippets/fastcgi-php.conf | 0 API/docker/{ => development}/php/Dockerfile | 6 +-- .../production}/docker-compose.yml | 14 +++--- API/docker/production/nginx/Dockerfile | 10 ++++ .../nginx/conf.d/default.conf | 0 .../nginx/snippets/fastcgi-php.conf | 5 ++ API/docker/production/php/Dockerfile | 50 +++++++++++++++++++ 10 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 API/docker/development/docker-compose.yml rename API/docker/{ => development}/nginx/Dockerfile (100%) create mode 100644 API/docker/development/nginx/conf.d/default.conf rename API/docker/{ => development}/nginx/snippets/fastcgi-php.conf (100%) rename API/docker/{ => development}/php/Dockerfile (90%) rename API/{ => docker/production}/docker-compose.yml (53%) create mode 100644 API/docker/production/nginx/Dockerfile rename API/docker/{ => production}/nginx/conf.d/default.conf (100%) create mode 100644 API/docker/production/nginx/snippets/fastcgi-php.conf create mode 100644 API/docker/production/php/Dockerfile diff --git a/API/docker/development/docker-compose.yml b/API/docker/development/docker-compose.yml new file mode 100644 index 0000000..d5d3dd4 --- /dev/null +++ b/API/docker/development/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' +services: + nginx: + build: + context: ../../ # Laravel Project root (API folder) + dockerfile: docker/development/nginx/Dockerfile + image: velocities/mynginx:latest # Use the remote image + volumes: + - /etc/letsencrypt:/etc/letsencrypt # Mount live certs into the container + - ./docker/development/nginx/conf.d:/etc/nginx/conf.d # Assuming your nginx configs are in ./docker/nginx/conf.d + - ./docker/development/nginx/snippets:/etc/nginx/snippets # If you have snippets to include + ports: + - "8080:80" # Map host port 8080 to container port 80 + - "8443:443" # Map host port 8443 to container port 443 + depends_on: + - app + app: + build: + context: ../../ # Laravel Project root (API folder) + dockerfile: docker/development/php/Dockerfile + image: velocities/myapp:latest # Use the remote image + volumes: + - ./database:/var/www/SpeedCart/API/database + - ./storage:/var/www/SpeedCart/API/storage + - ./bootstrap/cache:/var/www/SpeedCart/API/bootstrap/cache + - ./:/var/www/SpeedCart/API # Mount your source code (should make container restart upon save to code) + restart: always # Ensures the container restarts automatically if it stops + expose: + - "9000" diff --git a/API/docker/nginx/Dockerfile b/API/docker/development/nginx/Dockerfile similarity index 100% rename from API/docker/nginx/Dockerfile rename to API/docker/development/nginx/Dockerfile diff --git a/API/docker/development/nginx/conf.d/default.conf b/API/docker/development/nginx/conf.d/default.conf new file mode 100644 index 0000000..7978754 --- /dev/null +++ b/API/docker/development/nginx/conf.d/default.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name localhost; + + root /var/www/SpeedCart/API/public; + + index index.php index.html; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_pass app:9000; # Match your app service name and port + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~ /\.ht { + deny all; + } +} diff --git a/API/docker/nginx/snippets/fastcgi-php.conf b/API/docker/development/nginx/snippets/fastcgi-php.conf similarity index 100% rename from API/docker/nginx/snippets/fastcgi-php.conf rename to API/docker/development/nginx/snippets/fastcgi-php.conf diff --git a/API/docker/php/Dockerfile b/API/docker/development/php/Dockerfile similarity index 90% rename from API/docker/php/Dockerfile rename to API/docker/development/php/Dockerfile index 7b8c9db..6ebb5cc 100644 --- a/API/docker/php/Dockerfile +++ b/API/docker/development/php/Dockerfile @@ -33,12 +33,12 @@ RUN docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath gd intl zip # into your Docker image, making Composer available for dependency management) COPY --from=composer:2.1.3 /usr/bin/composer /usr/bin/composer -# Copy existing application directory contents from working directory +# Copy existing application directory contents from API directory # on host to /var/www/SpeedCart/API in the container -COPY . /var/www/SpeedCart/API +COPY ../../ /var/www/SpeedCart/API # Copy existing application directory permissions -COPY --chown=www-data:www-data . /var/www/SpeedCart/API +COPY --chown=www-data:www-data ../../ /var/www/SpeedCart/API # Change current user to www-data (used by web servers to improve security) USER www-data diff --git a/API/docker-compose.yml b/API/docker/production/docker-compose.yml similarity index 53% rename from API/docker-compose.yml rename to API/docker/production/docker-compose.yml index ca46571..fc189fe 100644 --- a/API/docker-compose.yml +++ b/API/docker/production/docker-compose.yml @@ -3,12 +3,12 @@ services: nginx: build: context: . - dockerfile: docker/nginx/Dockerfile + dockerfile: nginx/Dockerfile image: velocities/mynginx:latest # Use the remote image volumes: - /etc/letsencrypt:/etc/letsencrypt # Mount live certs into the container - - ./docker/nginx/conf.d:/etc/nginx/conf.d # Assuming your nginx configs are in ./docker/nginx/conf.d - - ./docker/nginx/snippets:/etc/nginx/snippets # If you have snippets to include + - ./nginx/conf.d:/etc/nginx/conf.d # Assuming your nginx configs are in ./docker/nginx/conf.d + - ./nginx/snippets:/etc/nginx/snippets # If you have snippets to include ports: - "8080:80" # Map host port 8080 to container port 80 - "8443:443" # Map host port 8443 to container port 443 @@ -17,11 +17,11 @@ services: app: build: context: . - dockerfile: docker/php/Dockerfile + dockerfile: php/Dockerfile image: velocities/myapp:latest # Use the remote image volumes: - - ./database:/var/www/SpeedCart/API/database - - ./storage:/var/www/SpeedCart/API/storage - - ./bootstrap/cache:/var/www/SpeedCart/API/bootstrap/cache + - ../../database:/var/www/SpeedCart/API/database + - ../../storage:/var/www/SpeedCart/API/storage + - ../../bootstrap/cache:/var/www/SpeedCart/API/bootstrap/cache expose: - "9000" diff --git a/API/docker/production/nginx/Dockerfile b/API/docker/production/nginx/Dockerfile new file mode 100644 index 0000000..c3f080b --- /dev/null +++ b/API/docker/production/nginx/Dockerfile @@ -0,0 +1,10 @@ +# nginx Dockerfile + +FROM nginx:alpine + +WORKDIR /etc/nginx + +# No need to COPY SSL certificates or other configuration info + +# Expose ports +EXPOSE 80 443 diff --git a/API/docker/nginx/conf.d/default.conf b/API/docker/production/nginx/conf.d/default.conf similarity index 100% rename from API/docker/nginx/conf.d/default.conf rename to API/docker/production/nginx/conf.d/default.conf diff --git a/API/docker/production/nginx/snippets/fastcgi-php.conf b/API/docker/production/nginx/snippets/fastcgi-php.conf new file mode 100644 index 0000000..4cb2c3a --- /dev/null +++ b/API/docker/production/nginx/snippets/fastcgi-php.conf @@ -0,0 +1,5 @@ +fastcgi_split_path_info ^(.+\.php)(/.+)$; +fastcgi_index index.php; +include fastcgi_params; +fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; +fastcgi_param PATH_INFO $fastcgi_path_info; diff --git a/API/docker/production/php/Dockerfile b/API/docker/production/php/Dockerfile new file mode 100644 index 0000000..6ebb5cc --- /dev/null +++ b/API/docker/production/php/Dockerfile @@ -0,0 +1,50 @@ +# Use the official PHP image as a base image +FROM php:8.2-fpm + +# Set working directory (all subsequent commands will be run in this directory) +WORKDIR /var/www/SpeedCart/API + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ + locales \ + jpegoptim optipng pngquant gifsicle \ + vim \ + git \ + curl \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + zip \ + unzip \ + libzip-dev \ + sqlite3 \ + libsqlite3-dev \ + --no-install-recommends + +# Clear cache (This cleans up the package cache to reduce the size of the Docker image) +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install PHP extensions required by Laravel +RUN docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath gd intl zip soap + +# Install Composer (copies the composer binary from the latest Composer image +# into your Docker image, making Composer available for dependency management) +COPY --from=composer:2.1.3 /usr/bin/composer /usr/bin/composer + +# Copy existing application directory contents from API directory +# on host to /var/www/SpeedCart/API in the container +COPY ../../ /var/www/SpeedCart/API + +# Copy existing application directory permissions +COPY --chown=www-data:www-data ../../ /var/www/SpeedCart/API + +# Change current user to www-data (used by web servers to improve security) +USER www-data + +# Expose port 9000 (the default port for PHP-FPM to listen on) and start php-fpm server +# (Even if you are using Nginx as a reverse proxy, PHP-FPM will still need to be exposed +# internally within the Docker network, which is why we need EXPOSE 9000) +EXPOSE 9000 +CMD ["php-fpm"] From 73a37502f538a74ecb402be6ed7d08a7a9f95e6b Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 18 Mar 2025 13:17:03 -0400 Subject: [PATCH 7/8] Update remaining shared functions and their usage in speedcart-react folder for massive refactor --- .../createSharingPermissions.ts | 6 +- .../deleteGroceryItem/deleteGroceryItem.ts | 8 +- .../deleteShoppingList/deleteShoppingList.ts | 6 +- .../fetchGroceryItems/fetchGroceryItems.ts | 6 +- .../fetchOwnedShoppingLists.ts | 6 +- .../fetchSharedShoppingLists.ts | 6 +- .../fetchShoppingList/fetchShoppingList.ts | 6 +- .../updateGroceryItem/updateGroceryItem.ts | 6 +- .../updateShoppingListTitle.ts | 6 +- .../src/hooks/AuthContext/AuthContext.tsx | 3 +- .../ShoppingListContext.tsx | 20 ++++- .../src/pages/Dashboard/Dashboard.tsx | 82 +++++++++---------- .../ShoppingListDetailWithProvider.tsx | 19 +++-- .../ShoppingListShare/ShoppingListShare.tsx | 8 +- 14 files changed, 123 insertions(+), 65 deletions(-) diff --git a/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts b/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts index ac6ce86..80efce2 100644 --- a/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts +++ b/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts @@ -1,6 +1,10 @@ import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; -export const createSharingPermissions = async (authToken = '', token: string) => { +export const createSharingPermissions: BackendFunction< + {token: string}, + Response +>= async (authToken = '', {token}) => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" diff --git a/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts b/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts index 7f9e9aa..9055c5a 100644 --- a/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts +++ b/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts @@ -1,7 +1,11 @@ import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; +import { BackendFunction } from "@types"; -export const deleteGroceryItem = async (authToken = '', item: GroceryItem) => { +export const deleteGroceryItem: BackendFunction< + {item: GroceryItem}, + Response +> = async (authToken = '', {item}) => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" @@ -11,7 +15,7 @@ export const deleteGroceryItem = async (authToken = '', item: GroceryItem) => { headers['Authorization'] = `Bearer ${authToken}`; } - fetch(`${BASE_URL}/grocery-items/${item.item_id}`, { + return fetch(`${BASE_URL}/grocery-items/${item.item_id}`, { method: 'DELETE', headers: headers, credentials: 'include', diff --git a/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts b/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts index b13fe3d..0e2c36b 100644 --- a/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts +++ b/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts @@ -1,6 +1,10 @@ import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from '@types'; -export const deleteShoppingList = async (authToken = '', listId: string) => { +export const deleteShoppingList: BackendFunction< + {listId: string}, + Response +> = async (authToken = '', {listId}) => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" diff --git a/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts b/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts index 9d6a706..9d3ba80 100644 --- a/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts +++ b/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts @@ -1,6 +1,10 @@ import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from '@types'; -export const fetchGroceryItems = async (authToken = '', listId: string) => { +export const fetchGroceryItems: BackendFunction< + {listId: string}, + Response +> = async (authToken = '', {listId}) => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" diff --git a/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts b/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts index 2280680..0b1aeab 100644 --- a/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts +++ b/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts @@ -1,6 +1,10 @@ import { BASE_URL } from '@constants'; +import { BackendFunction } from '@types'; -export const fetchOwnedShoppingLists = (authToken = '') => { +export const fetchOwnedShoppingLists: BackendFunction< + {}, + Response +> = (authToken = '') => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" diff --git a/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts b/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts index 6b7903e..f863bec 100644 --- a/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts +++ b/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts @@ -1,6 +1,10 @@ import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; -export const fetchSharedShoppingLists = (authToken = '') => { +export const fetchSharedShoppingLists: BackendFunction< + {}, + Response +> = (authToken = '') => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" diff --git a/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts b/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts index b7de45c..ea294bc 100644 --- a/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts +++ b/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts @@ -1,6 +1,10 @@ import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; -export const fetchShoppingList = async (authToken = '', listId: string) => { +export const fetchShoppingList: BackendFunction< + {listId: string}, + Response +> = async (authToken = '', {listId}) => { const url = `${BASE_URL}/shopping-lists/${listId}`; const headers: any = { 'Content-Type': 'application/json', diff --git a/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts b/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts index 38c86b4..f748239 100644 --- a/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts +++ b/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts @@ -1,7 +1,11 @@ import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; +import { BackendFunction } from "@types"; -export const updateGroceryItem = async (authToken = '', item: GroceryItem) => { +export const updateGroceryItem: BackendFunction< + {item: GroceryItem}, + Response +> = async (authToken = '', {item}) => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" diff --git a/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts b/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts index 20561a9..3f8c753 100644 --- a/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts +++ b/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts @@ -1,6 +1,10 @@ import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; -export const updateShoppingListTitle = async (authToken = '', shoppingListName: string, shoppingListId: string) => { +export const updateShoppingListTitle: BackendFunction< + {shoppingListName: string, shoppingListId: string}, + Response +> = async (authToken = '', {shoppingListName, shoppingListId}) => { const headers: any = { 'Content-Type': 'application/json', "Accept" : "application/json" diff --git a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx index 2a8ad76..730ceb7 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx +++ b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx @@ -65,11 +65,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) .then((data) => { // Handle the response data here - console.log('Response text: ' + JSON.stringify(data) + ' and data token: ' + JSON.stringify(data.token)); + //console.log('Response text: ' + JSON.stringify(data) + ' and data token: ' + JSON.stringify(data.token)); localStorage.setItem('speedcart_auth_bearer_token', JSON.stringify(data.token)); // Only needed for testing localStorage.setItem('speedcart_auth_token', data.token); - console.log("Data token retrieved: " + localStorage.getItem('speedcart_auth_token')); setAuthToken(data.token); }) .catch((error) => { diff --git a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx index 32b03b4..8195742 100644 --- a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx +++ b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx @@ -123,7 +123,10 @@ export const ShoppingListProvider = ({ children }) => { } else { // We're updating an existing list // Update shopping list title - const listResponse = await updateShoppingListTitle(authToken, shoppingList.name, listID.toString()); + const listResponse: Response = await callBackendAPI(updateShoppingListTitle, { + shoppingListName: shoppingList.name, + shoppingListId: listID.toString() + }); if (!listResponse.ok) { throw new Error('Failed to update shopping list title'); @@ -133,21 +136,30 @@ export const ShoppingListProvider = ({ children }) => { if (existingItems.length > 0) { // Update each existing grocery item - const itemPromises = existingItems.map(item =>updateGroceryItem(authToken, item)); + const itemPromises = existingItems.map(item => callBackendAPI(updateGroceryItem, { + item: item + })); await Promise.all(itemPromises); } if (deletedItems.length > 0) { // Remove each grocery item that the user wants to delete - const itemDeletePromises = deletedItems.map(item => deleteGroceryItem(authToken, item)); + const itemDeletePromises = deletedItems.map(item => callBackendAPI(deleteGroceryItem, { + item: item + })); await Promise.all(itemDeletePromises); } if (newItems.length > 0) { // Add each new item the user wants to add - const itemCreationPromises = newItems.map(item => createGroceryItem(authToken, { ...item, shopping_list_id: currentListID })); + const itemCreationPromises = newItems.map(item => callBackendAPI(createGroceryItem, { + item: { + ...item, + shopping_list_id: currentListID + } + })); await Promise.all(itemCreationPromises); } diff --git a/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx b/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx index d5c6778..e4b210a 100644 --- a/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx +++ b/Frontend/speedcart-react/src/pages/Dashboard/Dashboard.tsx @@ -32,50 +32,41 @@ function Dashboard() { useEffect(() => { document.title = "View shopping lists"; - if (!isAuthenticated) { - setOwnerListsAreLoading(false); - setSharedListsAreLoading(false); - setSharedListsError('You are not signed in; please try signing in at the Login page'); - setError('You are not signed in; please try signing in at the Login page'); - //return; - } else { + const showAllShoppingLists = async () => { setOwnerListsAreLoading(true); setSharedListsAreLoading(true); setSharedListsError(null); setError(null); - console.log('Starting queries with authToken: ' + authToken); // Retrieve lists owned by user - fetchOwnedShoppingLists(authToken) - .then(response => { - console.log('Owned lists response:', response); // Log the response object - if (!response.ok) { - if (response.status === 401) { + const ownedResponse: Response = await callBackendAPI(fetchOwnedShoppingLists, {}); + + try { + console.log('Owned lists response:', ownedResponse); // Log the response object + if (!ownedResponse.ok) { + if (ownedResponse.status === 401) { setOwnerListsAreLoading(false); logout(); throw new Error('Authorization error; please try signing in again at the Login page'); } else { - throw new Error('Network response was not ok with status ' + response.status); + throw new Error('Network response was not ok with status ' + ownedResponse.status); } } - return response.json(); - }) - .then(data => { - setShoppingListTitles(data); + const ownedData = await ownedResponse.json(); + setShoppingListTitles(ownedData); setOwnerListsAreLoading(false); - }) - .catch(error => { + } catch(error) { setError(error.toString()); setOwnerListsAreLoading(false); - }); + } // Retrieve lists shared with user - fetchSharedShoppingLists(authToken) - .then(response => { - if (!response.ok) { - console.log('Shared lists response:', response); // Log the response object + const sharedResponse: Response = await callBackendAPI(fetchSharedShoppingLists, {}); + try { + if (!sharedResponse.ok) { + console.log('Shared lists response:', sharedResponse); // Log the response object // We need to read this // Read and log the response body - const reader = response.body.getReader(); + const reader = sharedResponse.body.getReader(); reader.read().then(({ done, value }) => { if (!done) { const decoder = new TextDecoder(); @@ -86,27 +77,34 @@ function Dashboard() { console.error('Error reading response body:', err); }); - if (response.status === 401) { + if (sharedResponse.status === 401) { logout(); throw new Error('Authorization error; please try signing in again at the Login page'); } else { - throw new Error('Network response was not ok with status ' + response.status); + throw new Error('Network response was not ok with status ' + sharedResponse.status); } } - return response.json(); - }) - .then(data => { - setSharedShoppingListTitles(data); + const sharedData = await sharedResponse.json(); + setSharedShoppingListTitles(sharedData); setSharedListsAreLoading(false); - }) - .catch(error => { + } catch(error) { setSharedListsError(error.toString()); setSharedListsAreLoading(false); - }); + }; + } + + if (!isAuthenticated) { + setOwnerListsAreLoading(false); + setSharedListsAreLoading(false); + setSharedListsError('You are not signed in; please try signing in at the Login page'); + setError('You are not signed in; please try signing in at the Login page'); + //return; + } else { + showAllShoppingLists(); } }, [isAuthenticated, logout]); - const handleDelete = (listId: string) => { + const handleDelete = async (listId: string) => { if (!window.confirm('Are you sure you want to delete this?')) { return; } @@ -118,8 +116,11 @@ function Dashboard() { return; } - deleteShoppingList(authToken, listId) - .then(response => { + const response: Response = await callBackendAPI(deleteShoppingList, { + listId: listId + }); + + try { if (!response.ok) { if (response.status === 401) { throw new Error('Authorization error; please try signing in again at the Login page'); @@ -130,11 +131,10 @@ function Dashboard() { setDeletionStatus(RequestStatus.SUCCESS); // Filter out the deleted list from the state setShoppingListTitles(prevLists => prevLists.filter(list => list.list_id !== listId)); - }) - .catch(error => { + } catch (error) { setError(error.toString()); setDeletionStatus(RequestStatus.ERROR); - }); + } }; const handleSearchChange = (event) => { diff --git a/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx b/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx index 44f6f48..ef3a3f2 100644 --- a/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx +++ b/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx @@ -4,6 +4,8 @@ import { useParams } from 'react-router-dom'; import { fetchGroceryItems, fetchShoppingList, + useAuth, + AuthContextType } from 'shared'; import PageLayout from '@components/PageLayout'; @@ -39,12 +41,17 @@ const ShoppingListDetail = () => { handleUpdatedItemChange, handleRemoveExistingItem, handleSubmitListChanges } = useShoppingListContext(); + const { isAuthenticated, callBackendAPI }: AuthContextType = useAuth(); useEffect(() => { const fetchData = async () => { try { const authToken = localStorage.getItem('speedcart_auth_token'); - const listData = await fetchShoppingList(authToken, id); + console.log("FETCHING DATA") + const listData: any = await callBackendAPI(fetchShoppingList, { + listId: id + }); + console.log(`GOT DATA: ${listData.toString()}`) setShoppingList(listData); setListID(id); @@ -53,7 +60,9 @@ const ShoppingListDetail = () => { document.title = `Viewing list: ${listData.name}`; - const itemsDataResponse = await fetchGroceryItems(authToken, id); + const itemsDataResponse: Response = await callBackendAPI(fetchGroceryItems, { + listId: id + }); if (!itemsDataResponse.ok) { throw new Error(`Failed to fetch grocery items for shopping list with ID ${id}`); } @@ -72,10 +81,10 @@ const ShoppingListDetail = () => { } }; + if (isAuthenticated) + fetchData(); - fetchData(); - - }, [id]); + }, [id, isAuthenticated]); const resetChanges = () => { // Reset form changes diff --git a/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx b/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx index a3fa030..109474a 100644 --- a/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx +++ b/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx @@ -13,7 +13,7 @@ function ShoppingListShare() { const navigate = useNavigate(); const [shareInteractionStatus, setShareInteractionStatus] = useState("Loading..."); const { token } = useParams(); // Get link sharing token from url parameters - const { authToken, isAuthenticated, login } = useAuth(); + const { callBackendAPI, isAuthenticated, login } = useAuth(); useEffect(() => { // Only attempt to verify the share interaction if there's a token and the user is authenticated @@ -51,10 +51,12 @@ function ShoppingListShare() { verifyAndRedirect(); }, [token, isAuthenticated, navigate]); - const verifyShareInteraction = async (token) => { + const verifyShareInteraction = async (token: string) => { try { - const response = await createSharingPermissions(authToken, token); + const response: Response = await callBackendAPI(createSharingPermissions, { + token: token + }); //const responseText = await response.text(); //console.log("Response text:", responseText); From 0d14c0541034c121e0a6395da7fcee802f1f2435 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 18 Mar 2025 13:36:52 -0400 Subject: [PATCH 8/8] Remove previous testing code in ShoppingListContext, add some conditional code for testing frontend locally against deployed backend API, and reoptimize Dashboard list retrieval code to asynchronously grab owned and shared lists at the same time --- Frontend/shared/.env | 2 +- .../src/hooks/AuthContext/AuthContext.tsx | 10 +- .../src/hooks/AuthContext/AuthContextType.ts | 9 +- .../ShoppingListContext.tsx | 9 -- .../src/pages/Dashboard/Dashboard.tsx | 122 +++++++++--------- .../ShoppingListDetailWithProvider.tsx | 6 +- 6 files changed, 75 insertions(+), 83 deletions(-) diff --git a/Frontend/shared/.env b/Frontend/shared/.env index 47d84dd..62dcc2a 100644 --- a/Frontend/shared/.env +++ b/Frontend/shared/.env @@ -1,3 +1,3 @@ API_DOMAIN=api.speedcartapp.com API_PORT=8443 -TESTING_MODE=true \ No newline at end of file +TESTING_MODE=false \ No newline at end of file diff --git a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx index 730ceb7..ad3c681 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx +++ b/Frontend/shared/src/hooks/AuthContext/AuthContext.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { jwtDecode } from 'jwt-decode'; import { googleLogout } from '@react-oauth/google'; -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; import { AuthContextType } from './AuthContextType'; import { GoogleToken, BackendFunction } from '@types'; // Initialize the context with a default value of `null` @@ -68,8 +68,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children //console.log('Response text: ' + JSON.stringify(data) + ' and data token: ' + JSON.stringify(data.token)); localStorage.setItem('speedcart_auth_bearer_token', JSON.stringify(data.token)); // Only needed for testing - localStorage.setItem('speedcart_auth_token', data.token); - setAuthToken(data.token); + if (TESTING_MODE) { + localStorage.setItem('speedcart_auth_token', data.token); + setAuthToken(data.token); + } }) .catch((error) => { // Handle errors here @@ -116,7 +118,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children return endpointFunc(authToken, args); }; - return {children}; + return {children}; }; // Create a custom hook to use the AuthContext diff --git a/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts b/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts index cb58be2..c07b2c6 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts +++ b/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts @@ -2,13 +2,12 @@ export interface AuthContextType { isAuthenticated: boolean; loading: boolean; - authToken: string; userPictureLink: string | null; login: (token: string) => void; logout: () => void; // Adding the callBackendAPI function - callBackendAPI: ( - endpointFunc: (authToken: string, args: TArgs) => Promise, - args: TArgs - ) => Promise; + callBackendAPI: ( + endpointFunc: (authToken: string, args: TArgs) => Promise, + args: TArgs + ) => Promise; } \ No newline at end of file diff --git a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx index 8195742..f46c075 100644 --- a/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx +++ b/Frontend/speedcart-react/src/customHooks/ShoppingListContext/ShoppingListContext.tsx @@ -10,8 +10,6 @@ const ShoppingListContext = createContext(null); // that deal with saving shopping lists export const ShoppingListProvider = ({ children }) => { const { callBackendAPI }: AuthContextType = useAuth(); - // THIS ONE IS SPECIFICALLY ONLY NEEDED FOR TESTING - const [authToken, setAuthToken] = useState(''); const [shoppingListIDIsLoading, setShoppingListIDIsLoading] = useState(true); const [listTitle, setListTitle] = useState(''); const [listID, setListID] = useState(null); @@ -32,12 +30,6 @@ export const ShoppingListProvider = ({ children }) => { } }, [listID]); - useEffect(() => { - // Upon mount, we should be able to expect an authToken stored in localStorage - // (since none of the code in this file would even work if the user wasn't authenticated) - setAuthToken(localStorage.getItem('speedcart_auth_token')); - }, []); - // Handlers for all changes to state const handleNewItemChange = (index, newItem) => { const newItemsTemp = [...newItems]; @@ -168,7 +160,6 @@ export const ShoppingListProvider = ({ children }) => { return ( (false); const [canDelete, setCanDelete] = useState(false); - const { isAuthenticated, authToken, logout, callBackendAPI }: AuthContextType = useAuth(); + const { isAuthenticated, logout, callBackendAPI }: AuthContextType = useAuth(); - useEffect(() => { - document.title = "View shopping lists"; - - const showAllShoppingLists = async () => { - setOwnerListsAreLoading(true); - setSharedListsAreLoading(true); - setSharedListsError(null); - setError(null); - // Retrieve lists owned by user - const ownedResponse: Response = await callBackendAPI(fetchOwnedShoppingLists, {}); + const showOwnedShoppingLists = async () => { + setOwnerListsAreLoading(true); + setSharedListsAreLoading(true); + setSharedListsError(null); + setError(null); + // Retrieve lists owned by user + const ownedResponse: Response = await callBackendAPI(fetchOwnedShoppingLists, {}); - try { - console.log('Owned lists response:', ownedResponse); // Log the response object - if (!ownedResponse.ok) { - if (ownedResponse.status === 401) { - setOwnerListsAreLoading(false); - logout(); - throw new Error('Authorization error; please try signing in again at the Login page'); - } else { - throw new Error('Network response was not ok with status ' + ownedResponse.status); - } + try { + console.log('Owned lists response:', ownedResponse); // Log the response object + if (!ownedResponse.ok) { + if (ownedResponse.status === 401) { + setOwnerListsAreLoading(false); + logout(); + throw new Error('Authorization error; please try signing in again at the Login page'); + } else { + throw new Error('Network response was not ok with status ' + ownedResponse.status); } - const ownedData = await ownedResponse.json(); - setShoppingListTitles(ownedData); - setOwnerListsAreLoading(false); - } catch(error) { - setError(error.toString()); - setOwnerListsAreLoading(false); } + const ownedData = await ownedResponse.json(); + setShoppingListTitles(ownedData); + setOwnerListsAreLoading(false); + } catch(error) { + setError(error.toString()); + setOwnerListsAreLoading(false); + } + } - // Retrieve lists shared with user - const sharedResponse: Response = await callBackendAPI(fetchSharedShoppingLists, {}); - try { - if (!sharedResponse.ok) { - console.log('Shared lists response:', sharedResponse); // Log the response object - // We need to read this - // Read and log the response body - const reader = sharedResponse.body.getReader(); - reader.read().then(({ done, value }) => { - if (!done) { - const decoder = new TextDecoder(); - const text = decoder.decode(value); - console.log('Shared lists response body:', text); // Log the response body - } - }).catch(err => { - console.error('Error reading response body:', err); - }); - - if (sharedResponse.status === 401) { - logout(); - throw new Error('Authorization error; please try signing in again at the Login page'); - } else { - throw new Error('Network response was not ok with status ' + sharedResponse.status); + const showSharedShoppingLists = async () => { + // Retrieve lists shared with user + const sharedResponse: Response = await callBackendAPI(fetchSharedShoppingLists, {}); + try { + if (!sharedResponse.ok) { + console.log('Shared lists response:', sharedResponse); // Log the response object + // We need to read this + // Read and log the response body + const reader = sharedResponse.body.getReader(); + reader.read().then(({ done, value }) => { + if (!done) { + const decoder = new TextDecoder(); + const text = decoder.decode(value); + console.log('Shared lists response body:', text); // Log the response body } + }).catch(err => { + console.error('Error reading response body:', err); + }); + + if (sharedResponse.status === 401) { + logout(); + throw new Error('Authorization error; please try signing in again at the Login page'); + } else { + throw new Error('Network response was not ok with status ' + sharedResponse.status); } - const sharedData = await sharedResponse.json(); - setSharedShoppingListTitles(sharedData); - setSharedListsAreLoading(false); - } catch(error) { - setSharedListsError(error.toString()); - setSharedListsAreLoading(false); - }; - } + } + const sharedData = await sharedResponse.json(); + setSharedShoppingListTitles(sharedData); + setSharedListsAreLoading(false); + } catch(error) { + setSharedListsError(error.toString()); + setSharedListsAreLoading(false); + }; + } + + useEffect(() => { + document.title = "View shopping lists"; if (!isAuthenticated) { setOwnerListsAreLoading(false); @@ -100,7 +102,9 @@ function Dashboard() { setError('You are not signed in; please try signing in at the Login page'); //return; } else { - showAllShoppingLists(); + // These both run asynchronously for speed + showOwnedShoppingLists(); + showSharedShoppingLists(); } }, [isAuthenticated, logout]); diff --git a/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx b/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx index ef3a3f2..90b724a 100644 --- a/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx +++ b/Frontend/speedcart-react/src/pages/ShoppingListDetailWithProvider/ShoppingListDetailWithProvider.tsx @@ -46,12 +46,9 @@ const ShoppingListDetail = () => { useEffect(() => { const fetchData = async () => { try { - const authToken = localStorage.getItem('speedcart_auth_token'); - console.log("FETCHING DATA") const listData: any = await callBackendAPI(fetchShoppingList, { listId: id }); - console.log(`GOT DATA: ${listData.toString()}`) setShoppingList(listData); setListID(id); @@ -127,8 +124,7 @@ const ShoppingListDetail = () => { try { // Grab authentication token - const authToken = localStorage.getItem('speedcart_auth_exists'); - if (!authToken) { + if (!isAuthenticated) { throw new Error("You're not signed in; please go sign in first"); } await handleSubmitListChanges();