From be6480f549cf084f3ae9766c7250186d55cd458d Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 9 Aug 2024 18:08:25 +0100 Subject: [PATCH 1/5] refactor: edit product view --- .../_components/product-content-view.tsx | 30 +++++------- .../_components/product-grid-card.tsx | 2 +- .../_components/product-highlight-term.tsx | 8 +-- .../products/_components/product-list-row.tsx | 49 ++++++++----------- src/contexts/orgContext.tsx | 9 +++- 5 files changed, 45 insertions(+), 53 deletions(-) diff --git a/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx b/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx index b25a5a05d..8f6858a5d 100644 --- a/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx +++ b/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx @@ -1,5 +1,5 @@ import { AnimatePresence } from "framer-motion"; -import { useRouter } from "next-nprogress-bar"; +import { useRouter } from "next/navigation"; import { Table, @@ -9,7 +9,6 @@ import { TableRow, } from "~/components/ui/table"; import { useOrgContext } from "~/contexts/orgContext"; -import { useProductModal } from "~/hooks/admin-product/use-product.modal"; import { cn } from "~/lib/utils"; import { Product } from "~/types"; import { ProductGridCard } from "./product-grid-card"; @@ -29,32 +28,32 @@ export const ProductContentView = ({ searchTerm, view = "grid", }: Properties) => { - const { products } = useOrgContext(); const { - updateOpen, - updateProductId, - product_id, + products, isActionModal, setIsActionModal, + updateOpen, setIsDelete, - } = useProductModal(); + + setSelectedProduct, + } = useOrgContext(); const router = useRouter(); if (!products) return; const handleOpenActionModal = (product_id: string) => { - updateProductId(product_id); + setSelectedProduct(product_id); setIsActionModal(!isActionModal); }; const handleOpenDetail = (product_id: string) => { setIsActionModal(false); - updateProductId(product_id); + setSelectedProduct(product_id); updateOpen(true); }; const handleDeleteAction = (id: string) => { setIsActionModal(false); - updateProductId(id); + setSelectedProduct(id); setIsDelete(true); }; const handleEditAction = (id: string) => { @@ -99,24 +98,17 @@ export const ProductContentView = ({ subset.map((product, index) => ( setIsActionModal(false)} onOpenDetails={() => handleOpenDetail(product.id)} onEdit={() => handleEditAction(product.id)} onDelete={() => handleDeleteAction(product.id)} onOpenActionModal={() => handleOpenActionModal(product.id)} - onCloseActionModal={() => setIsActionModal(false)} - status={"in_stock"} - imgSrc={""} + {...product} /> ))} diff --git a/src/app/dashboard/(user-dashboard)/products/_components/product-grid-card.tsx b/src/app/dashboard/(user-dashboard)/products/_components/product-grid-card.tsx index 672636f7d..e7f0d335b 100644 --- a/src/app/dashboard/(user-dashboard)/products/_components/product-grid-card.tsx +++ b/src/app/dashboard/(user-dashboard)/products/_components/product-grid-card.tsx @@ -32,7 +32,7 @@ export function ProductGridCard({

- +

{formatPrice(price)}

diff --git a/src/app/dashboard/(user-dashboard)/products/_components/product-highlight-term.tsx b/src/app/dashboard/(user-dashboard)/products/_components/product-highlight-term.tsx index 791d10d98..f1e612241 100644 --- a/src/app/dashboard/(user-dashboard)/products/_components/product-highlight-term.tsx +++ b/src/app/dashboard/(user-dashboard)/products/_components/product-highlight-term.tsx @@ -2,11 +2,11 @@ import { cn } from "~/lib/utils"; type ProductHighlightTermProperties = { searchTerm: string; - title: string; + name: string; }; export function ProductHighlightTerm({ - title, + name, searchTerm = "", }: ProductHighlightTermProperties) { return ( @@ -18,7 +18,7 @@ export function ProductHighlightTerm({ searchTerm.length > 2 ? "w-[50px] overflow-x-auto" : "", )} dangerouslySetInnerHTML={{ - __html: title.replaceAll( + __html: name.replaceAll( new RegExp(`(${searchTerm})`, "gi"), (match, group) => `{title} + {name} )} ); diff --git a/src/app/dashboard/(user-dashboard)/products/_components/product-list-row.tsx b/src/app/dashboard/(user-dashboard)/products/_components/product-list-row.tsx index 1a885a289..85f7b0ffd 100644 --- a/src/app/dashboard/(user-dashboard)/products/_components/product-list-row.tsx +++ b/src/app/dashboard/(user-dashboard)/products/_components/product-list-row.tsx @@ -6,20 +6,14 @@ import BlurImage from "~/components/miscellaneous/blur-image"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { TableCell, TableRow } from "~/components/ui/table"; +import { useOrgContext } from "~/contexts/orgContext"; import { cn, formatPrice } from "~/lib/utils"; +import { Product } from "~/types"; import { ProductHighlightTerm } from "./product-highlight-term"; -type ProductListRowProperties = { - id: string; - title: string; - description: string; - category: string; - status: "in_stock" | "out_of_stock" | "low_on_stock"; - price: number; - imgSrc: string; +interface ProductListRowProperties extends Product { + searchTerm: string; isBottomRow: boolean; - selectedId?: string; - searchTerm?: string; isActionModal: boolean; onOpenActionModal: () => void; onCloseActionModal: () => void; @@ -27,17 +21,13 @@ type ProductListRowProperties = { onDelete: () => void; // (id: string) => void; onSelect?: () => void; // (id: string) => void; onOpenDetails: () => void; -}; +} export function ProductListRow({ id, - selectedId, - status, price, - imgSrc, - title, category, - onOpenDetails, + image, searchTerm = "", isBottomRow, isActionModal, @@ -45,13 +35,16 @@ export function ProductListRow({ onOpenActionModal, onEdit, onDelete, - // onSelect, + name, + stock_status, + onOpenDetails, }: ProductListRowProperties) { + const { selectedProduct } = useOrgContext(); const modalReference = useRef(null); useEffect(() => { if (!isActionModal || !modalReference.current) return; modalReference.current.scrollIntoView({ behavior: "smooth" }); - // handle click outside modal + const handleOutsideClick = (event: MouseEvent) => { if ( modalReference.current && @@ -70,7 +63,7 @@ export function ProductListRow({ key={id} className={cn( "relative bg-white", - selectedId === id ? "bg-[#F1F5F9]" : "", + selectedProduct === id ? "bg-[#F1F5F9]" : "", )} > @@ -80,7 +73,7 @@ export function ProductListRow({ className="sticky left-0 size-4 min-[500px]:size-5 lg:size-8" /> - + - {status === "in_stock" && "In Stock"} - {status === "low_on_stock" && "Low on Stock"} - {status === "out_of_stock" && "Out of Stock"} + {stock_status === "in stock" && "In Stock"} + {stock_status === "preorder" && "Low on Stock"} + {stock_status === "out of stock" && "Out of Stock"} @@ -139,7 +132,7 @@ export function ProductListRow({ - {isActionModal && selectedId === id && ( + {isActionModal && selectedProduct === id && ( >; isOpen: boolean; updateOpen: React.Dispatch>; + isActionModal: boolean; + setIsActionModal: React.Dispatch>; } export const OrgContext = createContext({} as OrgContextProperties); @@ -50,8 +52,9 @@ const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { const [isNewModal, setIsNewModal] = useState(false); const [isDelete, setIsDelete] = useState(false); const [isOpen, updateOpen] = useState(false); + const [isActionModal, setIsActionModal] = useState(false); - const isAnyModalOpen = isNewModal || isDelete || isOpen; + const isAnyModalOpen = isNewModal || isDelete || isOpen || isActionModal; useLayoutEffect(() => { startTransition(() => { @@ -81,6 +84,7 @@ const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { setIsNewModal(false); setIsDelete(false); updateOpen(false); + setIsActionModal(false); } }; @@ -115,6 +119,8 @@ const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { setIsDelete, isOpen, updateOpen, + isActionModal, + setIsActionModal, }), [ isLoading, @@ -126,6 +132,7 @@ const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { isNewModal, isDelete, isOpen, + isActionModal, ], ); From e2265dab9c94ccc77a7253b0fce3dd256708f8ee Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 9 Aug 2024 20:38:37 +0100 Subject: [PATCH 2/5] fix: super-admin route protection --- .../_components/product-content-view.tsx | 2 +- src/lib/auth.ts | 1 + src/lib/routes.ts | 2 ++ src/middleware.ts | 36 +++++++++++++------ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx b/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx index 8f6858a5d..ef7552c6d 100644 --- a/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx +++ b/src/app/dashboard/(user-dashboard)/products/_components/product-content-view.tsx @@ -1,5 +1,5 @@ import { AnimatePresence } from "framer-motion"; -import { useRouter } from "next/navigation"; +import { useRouter } from "next-nprogress-bar"; import { Table, diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 11f27ce25..959040d6e 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -20,6 +20,7 @@ declare module "next-auth" { email: User["email"]; image: User["avatar_url"]; role: User["role"]; + is_superadmin: boolean; } & DefaultSession["user"]; access_token?: string; } diff --git a/src/lib/routes.ts b/src/lib/routes.ts index 6b5e2a8f8..06e820b79 100644 --- a/src/lib/routes.ts +++ b/src/lib/routes.ts @@ -25,6 +25,8 @@ export const authRoutes = [ "/recovery", ]; +export const superAdminRoutes = ["/dashboard/admin/faqs"]; + export const apiAuthPrefix = "/api/"; /** diff --git a/src/middleware.ts b/src/middleware.ts index f71fe0d50..8200018fd 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,16 +1,25 @@ import { NextRequest, NextResponse } from "next/server"; -import { apiAuthPrefix, authRoutes, publicRoutes } from "~/lib/routes"; +import { + apiAuthPrefix, + authRoutes, + publicRoutes, + superAdminRoutes, +} from "~/lib/routes"; +import { auth } from "./lib/auth"; const NEXT_PUBLIC_ROOT_DOMAIN = "staging.nextjs.boilerplate.hng.tech"; + export default async function middleware(request: NextRequest) { const { nextUrl } = request; + const session = await auth(); const isLoggedIn = true; const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix); const isPublicRoute = publicRoutes.includes(nextUrl.pathname); const isAuthRoute = authRoutes.includes(nextUrl.pathname); + const isSuperAdminRoute = superAdminRoutes.includes(nextUrl.pathname); if (isApiAuthRoute || isPublicRoute) return; @@ -26,15 +35,22 @@ export default async function middleware(request: NextRequest) { searchParameters.length > 0 ? `?${searchParameters}` : "" }`; - //rewrites for dashboard pages and dev subdomains - if (hostname == `dashboard.${NEXT_PUBLIC_ROOT_DOMAIN}`) { - if (!isLoggedIn && !isAuthRoute) { - return Response.redirect( - new URL(`/login?callbackUrl=${nextUrl.pathname}`, nextUrl), - ); - } else if (isLoggedIn && isAuthRoute) { - return Response.redirect(new URL("/", nextUrl)); - } + // Check if the user is not logged in and trying to access an auth route + if (!isLoggedIn && !isAuthRoute) { + return NextResponse.redirect( + new URL(`/login?callbackUrl=${nextUrl.pathname}`, nextUrl), + ); + } + + if (isLoggedIn && isAuthRoute) { + return NextResponse.redirect(new URL("/", nextUrl)); + } + + if (isSuperAdminRoute && !session?.user?.is_superadmin) { + return NextResponse.redirect(new URL("/dashboard", nextUrl)); + } + + if (hostname === `dashboard.${NEXT_PUBLIC_ROOT_DOMAIN}`) { return NextResponse.rewrite( new URL(`/dashboard${path === "/" ? "/" : path}`, request.url), ); From 694e504d918a8072035c93378daa439d2127871b Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 9 Aug 2024 21:18:02 +0100 Subject: [PATCH 3/5] fix: super-admin route protection --- src/actions/product.ts | 2 +- src/lib/routes.ts | 2 +- src/middleware.ts | 101 ++++++++++++++++++++++++++++++++--------- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/actions/product.ts b/src/actions/product.ts index 902185a88..c0ef2757c 100644 --- a/src/actions/product.ts +++ b/src/actions/product.ts @@ -1,4 +1,4 @@ -// // /api/v1/organisations/{orgId}/products +"use server"; import axios from "axios"; import { z } from "zod"; diff --git a/src/lib/routes.ts b/src/lib/routes.ts index 06e820b79..d585115a4 100644 --- a/src/lib/routes.ts +++ b/src/lib/routes.ts @@ -25,7 +25,7 @@ export const authRoutes = [ "/recovery", ]; -export const superAdminRoutes = ["/dashboard/admin/faqs"]; +export const superAdminRoutes = ["dashboard/admin/faqs"]; export const apiAuthPrefix = "/api/"; diff --git a/src/middleware.ts b/src/middleware.ts index 8200018fd..af47d7c66 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,3 +1,65 @@ +// import { NextRequest, NextResponse } from "next/server"; + +// import { +// apiAuthPrefix, +// authRoutes, +// publicRoutes, +// superAdminRoutes, +// } from "~/lib/routes"; +// import { auth } from "./lib/auth"; + +// const NEXT_PUBLIC_ROOT_DOMAIN = "staging.nextjs.boilerplate.hng.tech"; +// export default async function middleware(request: NextRequest) { +// const { nextUrl } = request; +// const session = await auth(); + +// const isLoggedIn = true; + +// const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix); +// const isPublicRoute = publicRoutes.includes(nextUrl.pathname); +// const isAuthRoute = authRoutes.includes(nextUrl.pathname); +// const isSuperAdminRoute = superAdminRoutes.includes(nextUrl.pathname); + +// if (isApiAuthRoute || isPublicRoute) return; + +// const url = request.nextUrl; +// let hostname = request.headers +// .get("host")! +// .replace(/\.localhost(:\d+)?/, `.${NEXT_PUBLIC_ROOT_DOMAIN}`); + +// hostname = hostname.replace("www.", ""); // remove www. from domain +// const searchParameters = request.nextUrl.searchParams.toString(); +// // Get the pathname of the request (e.g. /, /about, /blog/first-post) +// const path = `${url.pathname}${ +// searchParameters.length > 0 ? `?${searchParameters}` : "" +// }`; + +// //rewrites for dashboard pages and dev subdomains +// if (hostname == `dashboard.${NEXT_PUBLIC_ROOT_DOMAIN}`) { +// if (!isLoggedIn && !isAuthRoute) { +// return Response.redirect( +// new URL(`/login?callbackUrl=${nextUrl.pathname}`, nextUrl), +// ); +// } else if (isLoggedIn && isAuthRoute) { +// return Response.redirect(new URL("/", nextUrl)); +// } +// if (session?.user.is_superadmin !== true && isSuperAdminRoute) { +// return Response.redirect(new URL("/dashboard", nextUrl)); +// } +// return NextResponse.rewrite( +// new URL(`/dashboard${path === "/" ? "/" : path}`, request.url), +// ); +// } + +// return; +// } + +// // Optionally, don't invoke Middleware on some paths +// export const config = { +// // eslint-disable-next-line unicorn/prefer-string-raw +// matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], +// }; + import { NextRequest, NextResponse } from "next/server"; import { @@ -14,14 +76,15 @@ export default async function middleware(request: NextRequest) { const { nextUrl } = request; const session = await auth(); - const isLoggedIn = true; + const isLoggedIn = session !== null; + const isSuperAdmin = session?.user?.is_superadmin === true; const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix); const isPublicRoute = publicRoutes.includes(nextUrl.pathname); const isAuthRoute = authRoutes.includes(nextUrl.pathname); const isSuperAdminRoute = superAdminRoutes.includes(nextUrl.pathname); - if (isApiAuthRoute || isPublicRoute) return; + if (isApiAuthRoute || isPublicRoute) return NextResponse.next(); const url = request.nextUrl; let hostname = request.headers @@ -30,37 +93,31 @@ export default async function middleware(request: NextRequest) { hostname = hostname.replace("www.", ""); // remove www. from domain const searchParameters = request.nextUrl.searchParams.toString(); - // Get the pathname of the request (e.g. /, /about, /blog/first-post) const path = `${url.pathname}${ searchParameters.length > 0 ? `?${searchParameters}` : "" }`; - // Check if the user is not logged in and trying to access an auth route - if (!isLoggedIn && !isAuthRoute) { - return NextResponse.redirect( - new URL(`/login?callbackUrl=${nextUrl.pathname}`, nextUrl), - ); - } - - if (isLoggedIn && isAuthRoute) { - return NextResponse.redirect(new URL("/", nextUrl)); - } - - if (isSuperAdminRoute && !session?.user?.is_superadmin) { - return NextResponse.redirect(new URL("/dashboard", nextUrl)); - } - - if (hostname === `dashboard.${NEXT_PUBLIC_ROOT_DOMAIN}`) { + // Rewrites for dashboard pages and dev subdomains + if (hostname == `dashboard.${NEXT_PUBLIC_ROOT_DOMAIN}`) { + if (!isLoggedIn && !isAuthRoute) { + return NextResponse.redirect( + new URL(`/login?callbackUrl=${nextUrl.pathname}`, nextUrl), + ); + } else if (isLoggedIn && isAuthRoute) { + return NextResponse.redirect(new URL("/", nextUrl)); + } + if (isSuperAdminRoute && !isSuperAdmin) { + return NextResponse.redirect(new URL("/dashboard", nextUrl)); + } return NextResponse.rewrite( new URL(`/dashboard${path === "/" ? "/" : path}`, request.url), ); } - return; + return NextResponse.next(); } // Optionally, don't invoke Middleware on some paths export const config = { - // eslint-disable-next-line unicorn/prefer-string-raw - matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], + matcher: [String.raw`/((?!.+\.[\w]+$|_next).*)`, "/", "/(api|trpc)(.*)"], }; From f6bdc07be3b86c1998c597c8863f0f42a4b33be8 Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 9 Aug 2024 21:23:37 +0100 Subject: [PATCH 4/5] fix: super-admin route protection --- src/actions/product.ts | 2 +- src/lib/auth.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/product.ts b/src/actions/product.ts index c0ef2757c..c197cf40d 100644 --- a/src/actions/product.ts +++ b/src/actions/product.ts @@ -1,4 +1,4 @@ -"use server"; +"use server "; import axios from "axios"; import { z } from "zod"; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 959040d6e..3e7db9045 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -20,7 +20,7 @@ declare module "next-auth" { email: User["email"]; image: User["avatar_url"]; role: User["role"]; - is_superadmin: boolean; + is_superadmin?: boolean; } & DefaultSession["user"]; access_token?: string; } From 8a38b00f872c16edd004772e9e894623c9f3b1be Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 9 Aug 2024 21:25:41 +0100 Subject: [PATCH 5/5] fix: super-admin route protection --- src/middleware.ts | 62 ----------------------------------------------- 1 file changed, 62 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index af47d7c66..0c19ef644 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,65 +1,3 @@ -// import { NextRequest, NextResponse } from "next/server"; - -// import { -// apiAuthPrefix, -// authRoutes, -// publicRoutes, -// superAdminRoutes, -// } from "~/lib/routes"; -// import { auth } from "./lib/auth"; - -// const NEXT_PUBLIC_ROOT_DOMAIN = "staging.nextjs.boilerplate.hng.tech"; -// export default async function middleware(request: NextRequest) { -// const { nextUrl } = request; -// const session = await auth(); - -// const isLoggedIn = true; - -// const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix); -// const isPublicRoute = publicRoutes.includes(nextUrl.pathname); -// const isAuthRoute = authRoutes.includes(nextUrl.pathname); -// const isSuperAdminRoute = superAdminRoutes.includes(nextUrl.pathname); - -// if (isApiAuthRoute || isPublicRoute) return; - -// const url = request.nextUrl; -// let hostname = request.headers -// .get("host")! -// .replace(/\.localhost(:\d+)?/, `.${NEXT_PUBLIC_ROOT_DOMAIN}`); - -// hostname = hostname.replace("www.", ""); // remove www. from domain -// const searchParameters = request.nextUrl.searchParams.toString(); -// // Get the pathname of the request (e.g. /, /about, /blog/first-post) -// const path = `${url.pathname}${ -// searchParameters.length > 0 ? `?${searchParameters}` : "" -// }`; - -// //rewrites for dashboard pages and dev subdomains -// if (hostname == `dashboard.${NEXT_PUBLIC_ROOT_DOMAIN}`) { -// if (!isLoggedIn && !isAuthRoute) { -// return Response.redirect( -// new URL(`/login?callbackUrl=${nextUrl.pathname}`, nextUrl), -// ); -// } else if (isLoggedIn && isAuthRoute) { -// return Response.redirect(new URL("/", nextUrl)); -// } -// if (session?.user.is_superadmin !== true && isSuperAdminRoute) { -// return Response.redirect(new URL("/dashboard", nextUrl)); -// } -// return NextResponse.rewrite( -// new URL(`/dashboard${path === "/" ? "/" : path}`, request.url), -// ); -// } - -// return; -// } - -// // Optionally, don't invoke Middleware on some paths -// export const config = { -// // eslint-disable-next-line unicorn/prefer-string-raw -// matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], -// }; - import { NextRequest, NextResponse } from "next/server"; import {