diff --git a/API/app/Http/Controllers/GoogleAuthenticationController.php b/API/app/Http/Controllers/GoogleAuthenticationController.php index d915691..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,12 +66,29 @@ public function handleLogin(Request $request): Response if (DEBUG_MODE) { Log::debug("Logging in user: " . print_r($googleId, true)); } + + // 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; + + return response()->json([ + 'token' => $token, + 'status' => 'success' + ], 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/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"] 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'); diff --git a/Frontend/shared/.env b/Frontend/shared/.env index 2f01cf9..62dcc2a 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=false \ 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/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 fd287ea..30f1cdc 100644 --- a/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts +++ b/Frontend/shared/src/functions/createGroceryItem/createGroceryItem.ts @@ -1,13 +1,18 @@ -import { BASE_URL } from '@constants'; import { GroceryItem } from '@types'; +import { backendAuthFetch } from "../backendAuthFetch"; +import { BackendFunction } from "@types"; -export const createGroceryItem = async (item: GroceryItem) => { - return fetch(`${BASE_URL}/grocery-items`, { +export const createGroceryItem: BackendFunction< + { item: GroceryItem }, + Response +> = async (authToken = '', { item }) => { + + return backendAuthFetch( + `/grocery-items`, + { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify(item) - }); + 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 f097aa5..91f5d13 100644 --- a/Frontend/shared/src/functions/createShareLink/createShareLink.ts +++ b/Frontend/shared/src/functions/createShareLink/createShareLink.ts @@ -1,14 +1,20 @@ -import { BASE_URL } from '@constants'; +import { backendAuthFetch } from "../backendAuthFetch"; +import { BackendFunction } from "@types"; -export const createShareLink = async (shareListId: string, permissions: any) => { - return fetch(`${BASE_URL}/share/${shareListId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - 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/createSharingPermissions/createSharingPermissions.ts b/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts index 7ce2526..80efce2 100644 --- a/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts +++ b/Frontend/shared/src/functions/createSharingPermissions/createSharingPermissions.ts @@ -1,11 +1,22 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; + +export const createSharingPermissions: BackendFunction< + {token: string}, + Response +>= async (authToken = '', {token}) => { + 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..af10986 100644 --- a/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts +++ b/Frontend/shared/src/functions/createShoppingList/createShoppingList.ts @@ -1,15 +1,19 @@ -import { BASE_URL } from '@constants'; +import { backendAuthFetch } from "../backendAuthFetch"; +import { BackendFunction } from "@types"; -export const createShoppingList = async (name: string, routeId: any = null) => { - return fetch(`${BASE_URL}/shopping-lists`, { +export const createShoppingList: BackendFunction< + { name: string; routeId: any }, + Response +> = async (authToken = '', { name, routeId }) => { + return backendAuthFetch( + `/shopping-lists`, + { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', body: JSON.stringify({ name: name, route_id: routeId - }) - }); + }), + }, + authToken + ); }; \ 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..9055c5a 100644 --- a/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts +++ b/Frontend/shared/src/functions/deleteGroceryItem/deleteGroceryItem.ts @@ -1,12 +1,23 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; +import { BackendFunction } 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: BackendFunction< + {item: GroceryItem}, + Response +> = async (authToken = '', {item}) => { + 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: '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..0e2c36b 100644 --- a/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts +++ b/Frontend/shared/src/functions/deleteShoppingList/deleteShoppingList.ts @@ -1,11 +1,21 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from '@types'; -export const deleteShoppingList = async (listId: string) => { +export const deleteShoppingList: BackendFunction< + {listId: string}, + Response +> = async (authToken = '', {listId}) => { + 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..9d3ba80 100644 --- a/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts +++ b/Frontend/shared/src/functions/fetchGroceryItems/fetchGroceryItems.ts @@ -1,12 +1,22 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from '@types'; -export const fetchGroceryItems = async (listId: string) => { +export const fetchGroceryItems: BackendFunction< + {listId: string}, + Response +> = async (authToken = '', {listId}) => { + 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..0b1aeab 100644 --- a/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts +++ b/Frontend/shared/src/functions/fetchOwnedShoppingLists/fetchOwnedShoppingLists.ts @@ -1,11 +1,22 @@ import { BASE_URL } from '@constants'; +import { BackendFunction } from '@types'; + +export const fetchOwnedShoppingLists: BackendFunction< + {}, + Response +> = (authToken = '') => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } -export const fetchOwnedShoppingLists = () => { 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..f863bec 100644 --- a/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts +++ b/Frontend/shared/src/functions/fetchSharedShoppingLists/fetchSharedShoppingLists.ts @@ -1,11 +1,22 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; + +export const fetchSharedShoppingLists: BackendFunction< + {}, + Response +> = (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..ea294bc 100644 --- a/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts +++ b/Frontend/shared/src/functions/fetchShoppingList/fetchShoppingList.ts @@ -1,21 +1,30 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; -export const fetchShoppingList = async (listId: string) => { - const url = `${BASE_URL}/shopping-lists/${listId}`; +export const fetchShoppingList: BackendFunction< + {listId: string}, + Response +> = async (authToken = '', {listId}) => { + 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..f748239 100644 --- a/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts +++ b/Frontend/shared/src/functions/updateGroceryItem/updateGroceryItem.ts @@ -1,12 +1,23 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; import { GroceryItem } from '@types'; +import { BackendFunction } from "@types"; + +export const updateGroceryItem: BackendFunction< + {item: GroceryItem}, + Response +> = async (authToken = '', {item}) => { + const headers: any = { + 'Content-Type': 'application/json', + "Accept" : "application/json" + }; + + if (TESTING_MODE && authToken !== '') { + headers['Authorization'] = `Bearer ${authToken}`; + } -export const updateGroceryItem = async (item: GroceryItem) => { 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..3f8c753 100644 --- a/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts +++ b/Frontend/shared/src/functions/updateShoppingListTItle/updateShoppingListTitle.ts @@ -1,12 +1,22 @@ -import { BASE_URL } from '@constants'; +import { BASE_URL, TESTING_MODE } from '@constants'; +import { BackendFunction } from "@types"; -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: BackendFunction< + {shoppingListName: string, shoppingListId: string}, + Response +> = async (authToken = '', {shoppingListName, shoppingListId}) => { + 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..ad3c681 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 { BASE_URL } from '@constants'; -import { AuthContextType } from "./AuthContextType"; -import { GoogleToken } from "@types"; +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { jwtDecode } from 'jwt-decode'; +import { googleLogout } from '@react-oauth/google'; +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` 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,51 @@ 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 + if (TESTING_MODE) { + localStorage.setItem('speedcart_auth_token', data.token); + setAuthToken(data.token); + } }) .catch((error) => { // Handle errors here - console.error("Error:", error); + console.error('Error:', error); }); setIsAuthenticated(true); setUserPictureLink(userInfo.picture); @@ -79,33 +89,43 @@ 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}; + // 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 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..c07b2c6 100644 --- a/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts +++ b/Frontend/shared/src/hooks/AuthContext/AuthContextType.ts @@ -5,4 +5,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 d4aab68..f46c075 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(); const [shoppingListIDIsLoading, setShoppingListIDIsLoading] = useState(true); const [listTitle, setListTitle] = useState(''); const [listID, setListID] = useState(null); @@ -98,7 +99,11 @@ 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: Response = await callBackendAPI(createShoppingList, { + name: listTitle, + route_id: null, + }); + if (!shoppingListResponse.ok) { throw new Error(`HTTP error! status: ${shoppingListResponse.status}`); } @@ -110,7 +115,10 @@ 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: Response = await callBackendAPI(updateShoppingListTitle, { + shoppingListName: shoppingList.name, + shoppingListId: listID.toString() + }); if (!listResponse.ok) { throw new Error('Failed to update shopping list title'); @@ -120,21 +128,30 @@ 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 => 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(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({ ...item, shopping_list_id: currentListID })); + const itemCreationPromises = newItems.map(item => callBackendAPI(createGroceryItem, { + item: { + ...item, + shopping_list_id: currentListID + } + })); await Promise.all(itemCreationPromises); } @@ -142,7 +159,7 @@ export const ShoppingListProvider = ({ children }) => { }; return ( - (false); const [canDelete, setCanDelete] = useState(false); - const { isAuthenticated, logout }: AuthContextType = useAuth(); + const { isAuthenticated, logout, callBackendAPI }: AuthContextType = useAuth(); + + 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); + } + } + const ownedData = await ownedResponse.json(); + setShoppingListTitles(ownedData); + setOwnerListsAreLoading(false); + } catch(error) { + setError(error.toString()); + setOwnerListsAreLoading(false); + } + } + + 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); + }; + } useEffect(() => { document.title = "View shopping lists"; @@ -39,74 +102,13 @@ function Dashboard() { setError('You are not signed in; please try signing in at the Login page'); //return; } else { - setOwnerListsAreLoading(true); - setSharedListsAreLoading(true); - setSharedListsError(null); - setError(null); - console.log('Starting queries...'); - // Retrieve lists owned by user - fetchOwnedShoppingLists() - .then(response => { - console.log('Owned lists response:', response); // Log the response object - if (!response.ok) { - if (response.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); - } - } - return response.json(); - }) - .then(data => { - setShoppingListTitles(data); - setOwnerListsAreLoading(false); - }) - .catch(error => { - setError(error.toString()); - setOwnerListsAreLoading(false); - }); - - // Retrieve lists shared with user - fetchSharedShoppingLists() - .then(response => { - if (!response.ok) { - console.log('Shared lists response:', response); // Log the response object - // We need to read this - // Read and log the response body - const reader = response.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 (response.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); - } - } - return response.json(); - }) - .then(data => { - setSharedShoppingListTitles(data); - setSharedListsAreLoading(false); - }) - .catch(error => { - setSharedListsError(error.toString()); - setSharedListsAreLoading(false); - }); + // These both run asynchronously for speed + showOwnedShoppingLists(); + showSharedShoppingLists(); } }, [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 +120,11 @@ function Dashboard() { return; } - deleteShoppingList(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 +135,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) => { @@ -150,7 +154,8 @@ function Dashboard() { try { setShareLink('Generating link...'); - const response = await createShareLink(shareListId, { + const response: Response = await callBackendAPI(createShareLink, { + shareListId: 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..90b724a 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,11 +41,14 @@ const ShoppingListDetail = () => { handleUpdatedItemChange, handleRemoveExistingItem, handleSubmitListChanges } = useShoppingListContext(); + const { isAuthenticated, callBackendAPI }: AuthContextType = useAuth(); useEffect(() => { const fetchData = async () => { try { - const listData = await fetchShoppingList(id); + const listData: any = await callBackendAPI(fetchShoppingList, { + listId: id + }); setShoppingList(listData); setListID(id); @@ -52,7 +57,9 @@ const ShoppingListDetail = () => { document.title = `Viewing list: ${listData.name}`; - const itemsDataResponse = await fetchGroceryItems(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}`); } @@ -71,10 +78,10 @@ const ShoppingListDetail = () => { } }; + if (isAuthenticated) + fetchData(); - fetchData(); - - }, [id]); + }, [id, isAuthenticated]); const resetChanges = () => { // Reset form changes @@ -117,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(); diff --git a/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx b/Frontend/speedcart-react/src/pages/ShoppingListShare/ShoppingListShare.tsx index 4f617e6..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 { 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(token); + const response: Response = await callBackendAPI(createSharingPermissions, { + token: token + }); //const responseText = await response.text(); //console.log("Response text:", responseText); 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