From 4fb6d3f7407413c750f7f0eaf9114c366271ed7b Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:17:43 +0100 Subject: [PATCH] Add admin dashboard and middleware protection Introduces an admin dashboard page with cards for inviting vendors, banning users, and viewing recent orders. Adds middleware logic to restrict /admin routes to the 'admin1' user and updates route matching. Also updates git-info.json with latest commit metadata. --- app/admin/page.tsx | 74 +++++++++++++++++++++++++++ components/admin/BanUserCard.tsx | 61 ++++++++++++++++++++++ components/admin/InviteVendorCard.tsx | 62 ++++++++++++++++++++++ components/admin/RecentOrdersCard.tsx | 74 +++++++++++++++++++++++++++ middleware.ts | 20 +++++++- public/git-info.json | 4 +- 6 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 app/admin/page.tsx create mode 100644 components/admin/BanUserCard.tsx create mode 100644 components/admin/InviteVendorCard.tsx create mode 100644 components/admin/RecentOrdersCard.tsx diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..9e26fe5 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,74 @@ +export const dynamic = "force-dynamic"; + +import InviteVendorCard from "@/components/admin/InviteVendorCard"; +import BanUserCard from "@/components/admin/BanUserCard"; +import RecentOrdersCard from "@/components/admin/RecentOrdersCard"; + +export default function AdminPage() { + return ( +
+
+

Admin

+

Restricted area. Only admin1 can access.

+
+ +
+ +
+
+

System status

+

Uptime, versions, environment

+
+ OK +
+
+ + +
+
+

Logs

+

View recent errors and warnings

+
+ New +
+
+ + + + + + +
+
+

Broadcast

+

Send a message to users

+
+ Tools +
+
+ + +
+
+

Config

+

Feature flags and settings

+
+ Edit +
+
+ + +
+
+

Payments

+

Gateways and webhooks

+
+ Setup +
+
+
+
+ ); +} + + diff --git a/components/admin/BanUserCard.tsx b/components/admin/BanUserCard.tsx new file mode 100644 index 0000000..cc04ed8 --- /dev/null +++ b/components/admin/BanUserCard.tsx @@ -0,0 +1,61 @@ +"use client"; +import { useState } from "react"; +import { fetchClient } from "@/lib/api-client"; + +export default function BanUserCard() { + const [telegramUserId, setTelegramUserId] = useState(""); + const [reason, setReason] = useState(""); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(null); + + async function handleBan(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + setMessage(null); + try { + await fetchClient("/admin/ban", { + method: "POST", + body: { telegramUserId, reason } + }); + setMessage("User banned"); + setTelegramUserId(""); + setReason(""); + } catch (e: any) { + setMessage(e?.message || "Failed to ban user"); + } finally { + setLoading(false); + } + } + + return ( +
+

Ban User

+

Block abusive users by Telegram ID

+
+ setTelegramUserId(e.target.value)} + required + /> + setReason(e.target.value)} + /> + + {message &&

{message}

} +
+
+ ); +} + + diff --git a/components/admin/InviteVendorCard.tsx b/components/admin/InviteVendorCard.tsx new file mode 100644 index 0000000..0a26e4c --- /dev/null +++ b/components/admin/InviteVendorCard.tsx @@ -0,0 +1,62 @@ +"use client"; +import { useState } from "react"; +import { fetchClient } from "@/lib/api-client"; + +export default function InviteVendorCard() { + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(null); + + async function handleInvite(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + setMessage(null); + try { + await fetchClient("/admin/invitations", { + method: "POST", + body: { username, email } + }); + setMessage("Invitation sent"); + setUsername(""); + setEmail(""); + } catch (e: any) { + setMessage(e?.message || "Failed to send invitation"); + } finally { + setLoading(false); + } + } + + return ( +
+

Invite Vendor

+

Generate and send an invite

+
+ setUsername(e.target.value)} + required + /> + setEmail(e.target.value)} + type="email" + /> + + {message &&

{message}

} +
+
+ ); +} + + diff --git a/components/admin/RecentOrdersCard.tsx b/components/admin/RecentOrdersCard.tsx new file mode 100644 index 0000000..7d4c3b5 --- /dev/null +++ b/components/admin/RecentOrdersCard.tsx @@ -0,0 +1,74 @@ +"use client"; +import { useEffect, useState } from "react"; +import { fetchClient } from "@/lib/api-client"; + +interface OrderItem { + name: string; + quantity: number; +} + +interface Order { + orderId: number | string; + userId: number; + total: number; + createdAt: string; + items?: OrderItem[]; +} + +export default function RecentOrdersCard() { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let mounted = true; + (async () => { + try { + const data = await fetchClient("/admin/recent-orders"); + if (mounted) setOrders(data); + } catch (e: any) { + if (mounted) setError(e?.message || "Failed to load orders"); + } finally { + if (mounted) setLoading(false); + } + })(); + return () => { mounted = false; }; + }, []); + + return ( +
+

Recent Orders

+

Last 10 orders across stores

+ {loading ? ( +

Loading...

+ ) : error ? ( +

{error}

+ ) : orders.length === 0 ? ( +

No recent orders

+ ) : ( +
+ {orders.slice(0, 10).map((o) => ( +
+
+
Order #{o.orderId}
+
{new Date(o.createdAt).toLocaleString()}
+
+
+ User: {o.userId} · Total: {o.total} +
+ {o.items && o.items.length > 0 && ( +
    + {o.items.map((it, idx) => ( +
  • {it.name} × {it.quantity}
  • + ))} +
+ )} +
+ ))} +
+ )} +
+ ); +} + + diff --git a/middleware.ts b/middleware.ts index 321afb0..f08a510 100644 --- a/middleware.ts +++ b/middleware.ts @@ -52,7 +52,23 @@ export async function middleware(req: NextRequest) { return NextResponse.redirect(new URL("/auth/login", req.url)); } - console.log("Middleware: Auth check successful, proceeding to dashboard"); + console.log("Middleware: Auth check successful"); + + // Admin-only protection for /admin routes + const pathname = new URL(req.url).pathname; + if (pathname.startsWith('/admin')) { + try { + const user = await res.json(); + const username = user?.vendor?.username; + if (username !== 'admin1') { + console.log("Middleware: Non-admin attempted to access /admin, redirecting"); + return NextResponse.redirect(new URL("/dashboard", req.url)); + } + } catch (e) { + console.log("Middleware: Failed to parse user for admin check, redirecting to login"); + return NextResponse.redirect(new URL("/auth/login", req.url)); + } + } } catch (error) { console.error("Authentication validation failed:", error); return NextResponse.redirect(new URL("/auth/login", req.url)); @@ -62,5 +78,5 @@ export async function middleware(req: NextRequest) { } export const config = { - matcher: ["/dashboard/:path*"], + matcher: ["/dashboard/:path*", "/admin/:path*"], }; \ No newline at end of file diff --git a/public/git-info.json b/public/git-info.json index 947ea08..86dc10e 100644 --- a/public/git-info.json +++ b/public/git-info.json @@ -1,4 +1,4 @@ { - "commitHash": "864e1e9", - "buildTime": "2025-10-10T00:32:58.696Z" + "commitHash": "72821e5", + "buildTime": "2025-10-15T16:14:23.964Z" } \ No newline at end of file