diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c4a9410..03a84b9 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -5,9 +5,21 @@ import { useSession } from "next-auth/react"; import PluginImage from "@/components/PluginImage"; import { Users, Shield, BarChart3, AlertTriangle, CheckCircle, XCircle, - Search, Eye, Clock, Package, Activity, Loader2, Star + Search, Eye, Clock, Package, Activity, Loader2, Star, Filter, ShieldAlert } from "lucide-react"; +const PLUGIN_STATUSES = ["ALL", "APPROVED", "PENDING_REVIEW", "REJECTED", "SUSPENDED", "FLAGGED", "DRAFT"] as const; +const STATUS_COLORS: Record = { + APPROVED: { bg: "rgba(16,185,129,0.1)", color: "var(--status-success)", border: "rgba(16,185,129,0.3)" }, + PENDING_REVIEW: { bg: "rgba(245,158,11,0.1)", color: "var(--status-warning)", border: "rgba(245,158,11,0.3)" }, + PENDING: { bg: "rgba(245,158,11,0.1)", color: "var(--status-warning)", border: "rgba(245,158,11,0.3)" }, + REJECTED: { bg: "rgba(239,68,68,0.1)", color: "var(--status-error)", border: "rgba(239,68,68,0.3)" }, + SUSPENDED: { bg: "rgba(239,68,68,0.08)", color: "#f87171", border: "rgba(248,113,113,0.3)" }, + FLAGGED: { bg: "rgba(251,146,60,0.1)", color: "#fb923c", border: "rgba(251,146,60,0.3)" }, + DRAFT: { bg: "rgba(100,116,139,0.1)", color: "var(--text-muted)", border: "rgba(100,116,139,0.3)" }, +}; +const NEGATIVE_STATUSES = ["REJECTED", "SUSPENDED", "FLAGGED"]; + const TRUST_LEVELS = ["NEW", "TRUSTED", "FLAGGED", "ADMIN"]; const TRUST_COLORS: Record = { ADMIN: { bg: "rgba(139,92,246,0.1)", color: "#8b5cf6" }, @@ -25,9 +37,22 @@ export default function AdminPage() { const [stats, setStats] = useState({ users: 0, plugins: 0, builds: 0, pendingReviews: 0 }); const [userSearch, setUserSearch] = useState(""); const [pluginSearch, setPluginSearch] = useState(""); + const [pluginStatusFilter, setPluginStatusFilter] = useState("ALL"); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); + // Modal for rejecting plugins/versions from the Plugins tab + const [pluginRejectModal, setPluginRejectModal] = useState<{ + type: "plugin" | "version"; + pluginId: string; + versionId?: string; + name: string; + currentStatus: string; + targetStatus: string; + } | null>(null); + const [pluginRejectReason, setPluginRejectReason] = useState(""); + const [pluginRejectLoading, setPluginRejectLoading] = useState(false); + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; const token = (session?.user as any)?.apiToken || ""; @@ -121,12 +146,12 @@ export default function AdminPage() { } }; - const changeVersionStatus = async (pluginId: string, versionId: string, newStatus: string) => { + const changeVersionStatus = async (pluginId: string, versionId: string, newStatus: string, reason?: string) => { try { const res = await fetch(`${apiUrl}/api/v1/admin/versions/${versionId}/status`, { method: "PATCH", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, - body: JSON.stringify({ status: newStatus }) + body: JSON.stringify({ status: newStatus, ...(reason ? { reason } : {}) }) }); const json = await res.json(); if (json.success) { @@ -143,12 +168,12 @@ export default function AdminPage() { } catch { /* noop */ } }; - const changePluginStatus = async (pluginId: string, newStatus: string) => { + const changePluginStatus = async (pluginId: string, newStatus: string, reason?: string) => { try { const res = await fetch(`${apiUrl}/api/v1/admin/plugins/${pluginId}/status`, { method: "PATCH", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, - body: JSON.stringify({ status: newStatus }) + body: JSON.stringify({ status: newStatus, ...(reason ? { reason } : {}) }) }); const json = await res.json(); if (json.success) { @@ -157,6 +182,41 @@ export default function AdminPage() { } catch { /* noop */ } }; + // Intercept status changes: if changing TO a negative status, open rejection reason modal + const handlePluginStatusChange = (pluginId: string, currentStatus: string, newStatus: string, pluginName: string) => { + if (NEGATIVE_STATUSES.includes(newStatus) && currentStatus !== newStatus) { + setPluginRejectModal({ type: "plugin", pluginId, name: pluginName, currentStatus, targetStatus: newStatus }); + setPluginRejectReason(""); + } else { + changePluginStatus(pluginId, newStatus); + } + }; + + const handleVersionStatusChange = (pluginId: string, versionId: string, currentStatus: string, newStatus: string, versionLabel: string) => { + if (NEGATIVE_STATUSES.includes(newStatus) && currentStatus !== newStatus) { + setPluginRejectModal({ type: "version", pluginId, versionId, name: versionLabel, currentStatus, targetStatus: newStatus }); + setPluginRejectReason(""); + } else { + changeVersionStatus(pluginId, versionId, newStatus); + } + }; + + const confirmPluginReject = async () => { + if (!pluginRejectModal || !pluginRejectReason.trim()) return; + setPluginRejectLoading(true); + try { + if (pluginRejectModal.type === "plugin") { + await changePluginStatus(pluginRejectModal.pluginId, pluginRejectModal.targetStatus, pluginRejectReason.trim()); + } else if (pluginRejectModal.versionId) { + await changeVersionStatus(pluginRejectModal.pluginId, pluginRejectModal.versionId, pluginRejectModal.targetStatus, pluginRejectReason.trim()); + } + setPluginRejectModal(null); + setPluginRejectReason(""); + } finally { + setPluginRejectLoading(false); + } + }; + const toggleFeatured = async (pluginId: string) => { try { const res = await fetch(`${apiUrl}/api/v1/admin/plugins/${pluginId}/featured`, { @@ -459,10 +519,13 @@ export default function AdminPage() { )} {/* Plugins Tab */} - {!loading && tab === "plugins" && ( + {!loading && tab === "plugins" && (() => { + const filtered = pluginStatusFilter === "ALL" ? plugins : plugins.filter(p => p.status === pluginStatusFilter); + return (
-
-
+ {/* Search + Filter Bar */} +
+
Search
+ {/* Status Filter Pills */} +
+ + {PLUGIN_STATUSES.map(s => { + const isActive = pluginStatusFilter === s; + const sc = s !== "ALL" ? STATUS_COLORS[s] : null; + const count = s === "ALL" ? plugins.length : plugins.filter(p => p.status === s).length; + return ( + + ); + })} +
+ @@ -487,7 +577,9 @@ export default function AdminPage() { - {plugins.map((plugin: any) => ( + {filtered.map((plugin: any) => { + const psc = STATUS_COLORS[plugin.status] || STATUS_COLORS.DRAFT; + return ( {plugin.versions && plugin.versions.length > 0 && ( - - + )} - ))} - {plugins.length === 0 && ( + )})} + {filtered.length === 0 && ( )}
@@ -502,14 +594,15 @@ export default function AdminPage() {
+ {/* Quick reject button — only shown for approved plugins */} + {plugin.status === "APPROVED" && ( + + )}
-
- {plugin.versions.map((v: any) => ( -
- v{v.version} +
+
Versions
+
+ {plugin.versions.map((v: any) => { + const vsc = STATUS_COLORS[v.status] || STATUS_COLORS.DRAFT; + return ( +
+ v{v.version} + {v.status}
- ))} + )})}
- No plugins found + {pluginStatusFilter !== "ALL" ? `No ${pluginStatusFilter.replace("_", " ").toLowerCase()} plugins found` : "No plugins found"}
+ ); + })()} + + {/* Plugin/Version Rejection Reason Modal */} + {pluginRejectModal && ( +
{ setPluginRejectModal(null); setPluginRejectReason(""); }}> +
e.stopPropagation()}> +
+
+ +

+ {pluginRejectModal.targetStatus === "SUSPENDED" ? "Suspend" : pluginRejectModal.targetStatus === "FLAGGED" ? "Flag" : "Reject"} {pluginRejectModal.type === "version" ? "Version" : "Plugin"} +

+
+

+ Changing {pluginRejectModal.name} from{" "} + + {pluginRejectModal.currentStatus} + {" → "} + + {pluginRejectModal.targetStatus} + . + {pluginRejectModal.currentStatus === "APPROVED" && ( + + + This plugin/version is currently live and approved. This action will remove it from public availability. + + )} +

+ + +