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.
+
+
+
+
+ );
+}
+
+
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
+
+
+ );
+}
+
+
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
+
+
+ );
+}
+
+
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