diff --git a/src/app/(auth)/_actions/auth.ts b/src/app/(auth)/_actions/auth.ts index 47429e9..04843ca 100644 --- a/src/app/(auth)/_actions/auth.ts +++ b/src/app/(auth)/_actions/auth.ts @@ -7,6 +7,7 @@ import bcrypt from "bcrypt"; import db from "@/db/db"; import { cookies } from "next/headers"; import { JWTPayload } from "jose"; +import { revalidatePath } from "next/cache"; const authSchema = z.object({ email: z.string().email({ message: "Invalid email address" }).trim(), @@ -29,7 +30,8 @@ export type AuthState = { export async function signIn( prevState: AuthState, formData: FormData, - toSeller?: boolean + toSeller?: boolean, + origin?: string | null ): Promise { const result = authSchema.safeParse(Object.fromEntries(formData)); @@ -53,9 +55,9 @@ export async function signIn( await createSession(userFromDb.id, userFromDb.role); - if (toSeller) { - redirect("/admin"); - } + if (toSeller) redirect("/admin"); + + if (origin) redirect(origin); redirect("/"); } @@ -98,13 +100,27 @@ export async function signUp(prevState: AuthState, formData: FormData) { await createSession(newUser.id, newUser.role); - // TODO: Redirect to the origin at which the user was prompted to sign in redirect("/"); } -export async function logout() { +const protectedRoutes = [ + /^\/orders$/, + /^\/admin\/.*/, + /^\/products\/checkout$/, + /^\/products\/download\/.+$/, + /^\/profile$/, +]; + +export async function logout(pathname: string) { await deleteSession(); - // redirect("/sign-in"); + + const isProtectedRoute = protectedRoutes.some((route) => route.test(pathname)); + + if (isProtectedRoute) { + redirect("/sign-in"); + } else { + revalidatePath(pathname); + } } const updateSchema = z.object({ diff --git a/src/app/(auth)/_components/SignInForm.tsx b/src/app/(auth)/_components/SignInForm.tsx index e3b33bf..e8e0710 100644 --- a/src/app/(auth)/_components/SignInForm.tsx +++ b/src/app/(auth)/_components/SignInForm.tsx @@ -10,9 +10,13 @@ export function SignInForm() { const isSeller: boolean = searchParams.get("as") === "seller"; - // simple closure to pass the isSeller value to the signIn function + const encodedOrigin = searchParams.get("origin") || "/"; + + const origin = decodeURIComponent(encodedOrigin); + + // simple closure to pass the isSeller & origin value to the signIn function const signInWithSeller = async (prevState: AuthState, formData: FormData) => { - return signIn(prevState, formData, isSeller); + return signIn(prevState, formData, isSeller, origin); }; const [state, action] = useFormState(signInWithSeller, {}); diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index b48a7f1..2ee9b2f 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -13,10 +13,6 @@ export default function SignIn() { const isSeller: boolean = searchParams.get("as") === "seller"; const router = useRouter(); - - const origin = searchParams.get("origin"); - // TODO: redirect from example: cart page to sign-in - return (
Sign in diff --git a/src/app/(customerFacing)/_components/LogoutButton.tsx b/src/app/(customerFacing)/_components/LogoutButton.tsx deleted file mode 100644 index ccc66db..0000000 --- a/src/app/(customerFacing)/_components/LogoutButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { logout } from "@/app/(auth)/_actions/auth"; - -export default function LogoutButton({ isAuthenticated }: { isAuthenticated: boolean }) { - if (isAuthenticated) - return ( -
logout()}> - Logout -
- ); - return <>; -} diff --git a/src/app/(customerFacing)/layout.tsx b/src/app/(customerFacing)/layout.tsx index ca7bff8..9341622 100644 --- a/src/app/(customerFacing)/layout.tsx +++ b/src/app/(customerFacing)/layout.tsx @@ -2,7 +2,12 @@ import Cart from "@/app/(customerFacing)/_components/Cart"; import SearchBar from "@/components/SearchBar"; export const dynamic = "force-dynamic"; -import { CircleUser } from "lucide-react"; +import { + ChartNoAxesCombined, + CircleUser, + ShoppingBasket, + UserRoundPen, +} from "lucide-react"; import { cookies } from "next/headers"; import { decrypt } from "@/lib/session"; import { MobileNav, Nav, NavLink } from "@/components/Nav"; @@ -15,7 +20,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import Link from "next/link"; -import LogoutButton from "./_components/LogoutButton"; +import LogoutButton from "../../components/LogoutButton"; export default async function Layout({ children }: Readonly<{ children: ReactNode }>) { const cookie = cookies().get("session")?.value; @@ -43,8 +48,9 @@ export default async function Layout({ children }: Readonly<{ children: ReactNod ) )}
+
- + @@ -58,13 +64,31 @@ export default async function Layout({ children }: Readonly<{ children: ReactNod - Profile + + + Profile + - My Orders + + {" "} + My Orders + - Sales Dashboard + + + Sales Dashboard + diff --git a/src/app/(customerFacing)/products/[id]/page.tsx b/src/app/(customerFacing)/products/[id]/page.tsx index 0807e42..4a2cd7b 100644 --- a/src/app/(customerFacing)/products/[id]/page.tsx +++ b/src/app/(customerFacing)/products/[id]/page.tsx @@ -12,7 +12,7 @@ import db from "@/db/db"; import { cache } from "@/lib/cache"; import { formatCurrency } from "@/lib/formatter"; import { Product } from "@prisma/client"; -import { PackageSearch, Star } from "lucide-react"; +import { Download, PackageSearch, Star } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; @@ -101,12 +101,12 @@ export default async function ProductViewPage({
{product.priceInCents === 1 ? ( - // TODO : Download functionality ) : (
diff --git a/src/app/(customerFacing)/products/checkout/page.tsx b/src/app/(customerFacing)/products/checkout/page.tsx index b5daaf5..80a5aa4 100644 --- a/src/app/(customerFacing)/products/checkout/page.tsx +++ b/src/app/(customerFacing)/products/checkout/page.tsx @@ -16,8 +16,6 @@ export default async function CheckoutPage({ }: { searchParams: SearchParams; }) { - // TODO: handle error cases where params is empty or cart is empty, also handle case where product is null - const productIds = searchParams.pid; const ids = Array.isArray(productIds) ? productIds : [productIds]; diff --git a/src/app/(customerFacing)/products/download/free/[productId]/route.ts b/src/app/(customerFacing)/products/download/free/[productId]/route.ts new file mode 100644 index 0000000..61a4925 --- /dev/null +++ b/src/app/(customerFacing)/products/download/free/[productId]/route.ts @@ -0,0 +1,29 @@ +import db from "@/db/db"; +import fs from "fs/promises"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + req: NextRequest, + { params: { productId } }: { params: { productId: string } } +) { + const product = await db.product.findUnique({ + where: { id: productId }, + select: { filePath: true, name: true }, + }); + + if (product == null) + return NextResponse.redirect(new URL("/products/download/expired", req.url)); + + // TODO: create an order or something to track number of downloads + + const { size } = await fs.stat(product.filePath); + const file = await fs.readFile(product.filePath); + const extension = product.filePath.split(".").pop(); + + return new NextResponse(file, { + headers: { + "Content-Disposition": `attachment; filename="${product.name}.${extension}"`, + "Content-Length": size.toString(), + }, + }); +} diff --git a/src/app/(customerFacing)/products/page.tsx b/src/app/(customerFacing)/products/page.tsx index 40a78a6..ce1ea45 100644 --- a/src/app/(customerFacing)/products/page.tsx +++ b/src/app/(customerFacing)/products/page.tsx @@ -20,6 +20,7 @@ type ProductPageProps = { }; export default function ProductPage({ searchParams }: ProductPageProps) { + // TODO : on related pages, add a params as category on view all button return (
{searchParams.searchQuery && ( diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 7c2d3d8..a45a6c2 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,5 +1,5 @@ -import { Nav, NavItem, NavLink } from "@/components/Nav"; -import LogoutButton from "../(customerFacing)/_components/LogoutButton"; +import { MobileNav, Nav, NavItem, NavLink } from "@/components/Nav"; +import LogoutButton from "../../components/LogoutButton"; import { getCurrentUserFromSession } from "../(auth)/_actions/auth"; export const dynamic = "force-dynamic"; // We want to prevent caching in admin page, as we need the most updated data. @@ -13,30 +13,42 @@ export default async function AdminLayout({ const isAdmin = user?.role === "admin"; + const navItems = [ + { name: "Dashboard", href: "/admin", isVisible: true }, + { name: "Products", href: "/admin/products", isVisible: true }, + { name: "Sales", href: "/admin/orders", isVisible: true }, + { name: "Users", href: "/admin/users", isVisible: isAdmin }, + { + name: "Buy Products", + href: "/products", + isVisible: !isAdmin, + }, + ]; + return (
{children}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c310279..c411e6f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -103,9 +103,7 @@ export default async function AdminDashboard() { /> No customers found

; @@ -48,6 +58,7 @@ async function UsersTable() { Email + Products selling Orders Value @@ -59,6 +70,7 @@ async function UsersTable() { {users.map((user) => ( {user.email} + {formatNumber(user.products.length)} {formatNumber(user.orders.length)} {formatCurrency( diff --git a/src/components/LogoutButton.tsx b/src/components/LogoutButton.tsx new file mode 100644 index 0000000..35adc4d --- /dev/null +++ b/src/components/LogoutButton.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { logout } from "@/app/(auth)/_actions/auth"; +import { LogOut } from "lucide-react"; +import { usePathname } from "next/navigation"; + +export default function LogoutButton({ + isAuthenticated, + className, +}: { + isAuthenticated: boolean; + className?: string; +}) { + const pathname = usePathname(); + + const handleLogout = async () => { + await logout(pathname); + }; + + if (isAuthenticated) + return ( +
+ + Logout +
+ ); + return <>; +} diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index bc84c95..0ea0139 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -17,7 +17,7 @@ export function Nav({ return (