diff --git a/first-cdk-deployment/app.py b/first-cdk-deployment/app.py index c3348b587..d804ff752 100644 --- a/first-cdk-deployment/app.py +++ b/first-cdk-deployment/app.py @@ -1,31 +1,8 @@ #!/usr/bin/env python3 import os - import aws_cdk as cdk - -from first_cdk_deployment.first_cdk_deployment_stack import FirstCdkDeploymentStack - +from first_cdk_deployment.first_cdk_deployment_stack import WebsiteStack app = cdk.App() -FirstCdkDeploymentStack(app, "FirstCdkDeploymentStack", - # If you don't specify 'env', this stack will be environment-agnostic. - # Account/Region-dependent features and context lookups will not work, - # but a single synthesized template can be deployed anywhere. - - # Uncomment the next line to specialize this stack for the AWS Account - # and Region that are implied by the current CLI configuration. - - #env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')), - - # Uncomment the next line if you know exactly what Account and Region you - # want to deploy the stack to. */ - - #env=cdk.Environment(account='123456789012', region='us-east-1'), - - # For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html - ) - +WebsiteStack(app, "WebsiteStack") app.synth() - - -#!/usr/bin/env python3 diff --git a/first-cdk-deployment/cdk.context.json b/first-cdk-deployment/cdk.context.json new file mode 100644 index 000000000..f4bc90c75 --- /dev/null +++ b/first-cdk-deployment/cdk.context.json @@ -0,0 +1,5 @@ +{ + "acknowledged-issue-numbers": [ + 32775 + ] +} diff --git a/first-cdk-deployment/first_cdk_deployment/first_cdk_deployment_stack.py b/first-cdk-deployment/first_cdk_deployment/first_cdk_deployment_stack.py index 7a43f163c..bae384dc5 100644 --- a/first-cdk-deployment/first_cdk_deployment/first_cdk_deployment_stack.py +++ b/first-cdk-deployment/first_cdk_deployment/first_cdk_deployment_stack.py @@ -1,24 +1,3 @@ -# from aws_cdk import ( -# # Duration, -# Stack, -# # aws_sqs as sqs, -# ) -# from constructs import Construct - -# class FirstCdkDeploymentStack(Stack): - -# def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: -# super().__init__(scope, construct_id, **kwargs) - -# # The code that defines your stack goes here - -# # example resource -# # queue = sqs.Queue( -# # self, "FirstCdkDeploymentQueue", -# # visibility_timeout=Duration.seconds(300), -# # ) - - from aws_cdk import ( Stack, aws_s3 as s3, @@ -29,32 +8,49 @@ RemovalPolicy ) from constructs import Construct -import os -class FirstCdkDeploymentStack(Stack): +class WebsiteStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) - # Create an S3 bucket to store the website files + # Create an S3 bucket to host the website website_bucket = s3.Bucket( self, - "ShopReactBucket", - block_public_access=s3.BlockPublicAccess.BLOCK_ALL, - removal_policy=RemovalPolicy.DESTROY, - auto_delete_objects=True + "WebsiteBucket", + removal_policy=RemovalPolicy.DESTROY, # NOT recommended for production + auto_delete_objects=True, # NOT recommended for production + block_public_access=s3.BlockPublicAccess.BLOCK_ALL ) - # Create CloudFront distribution - distribution = cloudfront.Distribution( + # Create Origin Access Identity for CloudFront + origin_access_identity = cloudfront.OriginAccessIdentity( self, - "ShopReactDistribution", + "OriginAccessIdentity", + comment="CloudFront access to S3" + ) + + # Grant read permissions for CloudFront + website_bucket.grant_read(origin_access_identity) + + # Create CloudFront Distribution + distribution = cloudfront.Distribution( + self, + "Distribution", default_behavior=cloudfront.BehaviorOptions( - origin=origins.S3Origin(website_bucket), + origin=origins.S3Origin( + website_bucket, + origin_access_identity=origin_access_identity + ), viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - cache_policy=cloudfront.CachePolicy.CACHING_OPTIMIZED, + cache_policy=cloudfront.CachePolicy.CACHING_OPTIMIZED ), default_root_object="index.html", error_responses=[ + cloudfront.ErrorResponse( + http_status=403, + response_http_status=200, + response_page_path="/index.html" + ), cloudfront.ErrorResponse( http_status=404, response_http_status=200, @@ -63,11 +59,11 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ] ) - # Deploy site contents to S3 + # Deploy site contents to S3 bucket s3deploy.BucketDeployment( - self, - "DeployShopReact", - sources=[s3deploy.Source.asset(os.path.join(os.path.dirname(__file__), "..", "..", "dist"))], + self, + "DeployWebsite", + sources=[s3deploy.Source.asset("../dist")], # Adjust this path destination_bucket=website_bucket, distribution=distribution, distribution_paths=["/*"] @@ -75,7 +71,10 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: # Output the CloudFront URL CfnOutput( - self, - "DistributionDomainName", - value=distribution.distribution_domain_name + self, + "CloudFrontURL", + value=f"https://{distribution.distribution_domain_name}", + description="Website URL" ) + + diff --git a/first-cdk-deployment/requirements.txt b/first-cdk-deployment/requirements.txt index 20ba9d5fd..1d08a005f 100644 --- a/first-cdk-deployment/requirements.txt +++ b/first-cdk-deployment/requirements.txt @@ -1,2 +1,3 @@ -aws-cdk-lib==2.178.1 +aws-cdk-lib==2.124.0 constructs>=10.0.0,<11.0.0 +typeguard>=2.13.3 diff --git a/src/components/ToastProvider/ToastProvider.tsx b/src/components/ToastProvider/ToastProvider.tsx new file mode 100644 index 000000000..74d641f98 --- /dev/null +++ b/src/components/ToastProvider/ToastProvider.tsx @@ -0,0 +1,32 @@ +import React, { useState, useEffect } from "react"; +import { Snackbar, Alert } from "@mui/material"; + +const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(""); + const [severity, setSeverity] = useState<"error" | "warning" | "info" | "success">("error"); + + useEffect(() => { + const handleToast = (event: CustomEvent) => { + setMessage(event.detail.message); + setSeverity(event.detail.severity || "error"); + setOpen(true); + }; + + window.addEventListener("global-toast", handleToast as EventListener); + return () => window.removeEventListener("global-toast", handleToast as EventListener); + }, []); + + return ( + <> + {children} + setOpen(false)}> + setOpen(false)}> + {message} + + + + ); +}; + +export default ToastProvider; \ No newline at end of file diff --git a/src/components/pages/PageProducts/components/Products.tsx b/src/components/pages/PageProducts/components/Products.tsx index 68eec6d69..9f986554d 100755 --- a/src/components/pages/PageProducts/components/Products.tsx +++ b/src/components/pages/PageProducts/components/Products.tsx @@ -32,6 +32,7 @@ export default function Products() { {product.title} + Available: {count} {formatAsPrice(product.price)} diff --git a/src/components/pages/admin/PageProductImport/components/CSVFileImport.tsx b/src/components/pages/admin/PageProductImport/components/CSVFileImport.tsx index d11028c96..921b9d4ac 100755 --- a/src/components/pages/admin/PageProductImport/components/CSVFileImport.tsx +++ b/src/components/pages/admin/PageProductImport/components/CSVFileImport.tsx @@ -1,6 +1,8 @@ import React from "react"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; +import axios from "axios"; +import { Button, Snackbar, Alert } from "@mui/material"; type CSVFileImportProps = { url: string; @@ -8,13 +10,16 @@ type CSVFileImportProps = { }; export default function CSVFileImport({ url, title }: CSVFileImportProps) { - const [file, setFile] = React.useState(); + const [file, setFile] = React.useState(undefined); + const [error, setError] = React.useState<{ message: string; open: boolean }>({ + message: "", + open: false, + }); const onFileChange = (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { - const file = files[0]; - setFile(file); + setFile(files[0]); } }; @@ -23,25 +28,61 @@ export default function CSVFileImport({ url, title }: CSVFileImportProps) { }; const uploadFile = async () => { - console.log("uploadFile to", url); - - // Get the presigned URL - // const response = await axios({ - // method: "GET", - // url, - // params: { - // name: encodeURIComponent(file.name), - // }, - // }); - // console.log("File to upload: ", file.name); - // console.log("Uploading to: ", response.data); - // const result = await fetch(response.data, { - // method: "PUT", - // body: file, - // }); - // console.log("Result: ", result); - // setFile(""); + if (!file) { + console.error("No file selected."); + return; + } + + // Retrieve the token from local storage + const token = localStorage.getItem("authorization_token"); + console.log("Authorization Token:", token); // Log the token + + // Set up the headers with an empty authorization if token is null + const headers = { + Authorization: token ? `Basic ${token}` : "", // Allow empty header + }; + + console.log("Uploading file to", url); + + try { + // Get the presigned URL + const response = await axios.get(url, { + params: { name: encodeURIComponent(file.name) }, + headers, + }); + + console.log("File to upload: ", file.name); + console.log("Uploading to: ", response.data); + + // Upload file to the signed URL + const result = await fetch(response.data, { + method: "PUT", + body: file, + headers: { + "Content-Type": "text/csv", + }, + }); + + console.log("Result: ", result); + setFile(undefined); // Clear the file after successful upload + } catch (error: any) { + if (axios.isAxiosError(error)) { + // Handle specific status codes + const status = error.response?.status; + if (status === 401) { + setError({ message: "Unauthorized. Please sign in.", open: true }); + } else if (status === 403) { + setError({ message: "You don't have permission to upload this file.", open: true }); + } else { + setError({ message: "An error occurred while uploading the file.", open: true }); + } + } else { + setError({ message: "An unexpected error occurred.", open: true }); + } + console.error("Error uploading file:", error); + } }; + return ( @@ -51,10 +92,29 @@ export default function CSVFileImport({ url, title }: CSVFileImportProps) { ) : (
- - + +
)} + + {/* Error Snackbar */} + setError({ ...error, open: false })} + anchorOrigin={{ vertical: "top", horizontal: "center" }} + > + setError({ ...error, open: false })} + severity="error" + > + {error.message} + +
); } diff --git a/src/constants/apiPaths.ts b/src/constants/apiPaths.ts index 57e1c8f6b..9e95ae8a1 100755 --- a/src/constants/apiPaths.ts +++ b/src/constants/apiPaths.ts @@ -1,9 +1,11 @@ const API_PATHS = { //product: "https://.execute-api.eu-west-1.amazonaws.com/dev",https://pj8m5nmg5m.execute-api.us-east-1.amazonaws.com/prod/products/ //product: "https://i58jmttgyj.execute-api.us-east-1.amazonaws.com/prod/products",https://pj8m5nmg5m.execute-api.us-east-1.amazonaws.com/prod/products - product: "https://pj8m5nmg5m.execute-api.us-east-1.amazonaws.com/prod", + // product: "https://pj8m5nmg5m.execute-api.us-east-1.amazonaws.com/prod",https://c00aj28k2m.execute-api.us-east-1.amazonaws.com/prod/products + // import: "https://5uj67empu0.execute-api.us-east-1.amazonaws.com/prod", + product: "https://c00aj28k2m.execute-api.us-east-1.amazonaws.com/prod", order: "https://.execute-api.eu-west-1.amazonaws.com/dev", - import: "https://.execute-api.eu-west-1.amazonaws.com/dev", + import: "https://tpv5ydqmbg.execute-api.us-east-1.amazonaws.com/prod", bff: "https://.execute-api.eu-west-1.amazonaws.com/dev", cart: "https://.execute-api.eu-west-1.amazonaws.com/dev", }; diff --git a/src/index.tsx b/src/index.tsx index 5a3ad0b15..33d4e72c5 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,20 +4,27 @@ import App from "~/components/App/App"; import CssBaseline from "@mui/material/CssBaseline"; import { ThemeProvider } from "@mui/material/styles"; import { BrowserRouter } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "react-query"; +import { + QueryClient, + QueryClientProvider, + MutationCache +} from "react-query"; import { ReactQueryDevtools } from "react-query/devtools"; import { theme } from "~/theme"; +import "headers-polyfill"; +import ToastProvider from "./components/ToastProvider/ToastProvider"; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: false, staleTime: Infinity }, }, + mutationCache: new MutationCache({ + onError: (error: any) => { + window.dispatchEvent(new CustomEvent("global-toast", { detail: { message: error.message, severity: "error" } })); + }, + }), }); -if (import.meta.env.DEV) { - const { worker } = await import("./mocks/browser"); - worker.start({ onUnhandledRequest: "bypass" }); -} const container = document.getElementById("app"); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -28,10 +35,12 @@ root.render( - + + + -); +); \ No newline at end of file diff --git a/src/queries/products.ts b/src/queries/products.ts index b24ce6d54..114886f50 100644 --- a/src/queries/products.ts +++ b/src/queries/products.ts @@ -5,12 +5,20 @@ import { useQuery, useQueryClient, useMutation } from "react-query"; import React from "react"; export function useAvailableProducts() { + + const token = localStorage.getItem("authorization_token"); + console.log("Authorization Token:", token); return useQuery( "available-products", async () => { const res = await axios.get( // `${API_PATHS.bff}/product/available` - `${API_PATHS.product}/products` + `${API_PATHS.product}/products`, + { + headers: { + Authorization: `Basic ${token}`, + }, + } ); return res.data; } @@ -50,7 +58,7 @@ export function useRemoveProductCache() { export function useUpsertAvailableProduct() { return useMutation((values: AvailableProduct) => - axios.put(`${API_PATHS.bff}/product`, values, { + axios.put(`${API_PATHS.product}/product`, values, { headers: { Authorization: `Basic ${localStorage.getItem("authorization_token")}`, }, @@ -60,7 +68,7 @@ export function useUpsertAvailableProduct() { export function useDeleteAvailableProduct() { return useMutation((id: string) => - axios.delete(`${API_PATHS.bff}/product/${id}`, { + axios.delete(`${API_PATHS.product}/product/${id}`, { headers: { Authorization: `Basic ${localStorage.getItem("authorization_token")}`, },