From 2a4ea8b7bf653ee7a0a0350b1fdeb70b60ab8831 Mon Sep 17 00:00:00 2001 From: qiqi1616 <760960012@qq.com> Date: Sun, 17 May 2026 22:47:19 +0800 Subject: [PATCH 1/4] feat: implement full admin panel (services) --- apps/api/src/services/adminService.js | 76 ++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/apps/api/src/services/adminService.js b/apps/api/src/services/adminService.js index 9075111aaa..642377a6f1 100644 --- a/apps/api/src/services/adminService.js +++ b/apps/api/src/services/adminService.js @@ -1,8 +1,72 @@ +import { prisma } from "../config/db.js"; + export async function getAdminMetrics() { - return { - openJobs: 42, - activeFreelancers: 185, - flaggedAccounts: 3, - monthlyVolume: 128900 - }; + const [users, jobs, openJobs, disputes, openDisputes] = await Promise.all([ + prisma.user.count(), prisma.job.count(), + prisma.job.count({ where: { status: "OPEN" } }), + prisma.dispute.count(), + prisma.dispute.count({ where: { status: { in: ["OPEN", "UNDER_REVIEW"] } } }), + ]); + return { totalUsers: users, totalJobs: jobs, openJobs, totalDisputes: disputes, openDisputes }; } + +export async function listUsers({ page = "1", limit = "20", role, search }) { + const where = {}; + if (role) where.role = role; + if (search) where.OR = [{ name: { contains: search } }, { email: { contains: search } }]; + const skip = (Number(page) - 1) * Number(limit); + const [users, total] = await Promise.all([ + prisma.user.findMany({ where, skip, take: Number(limit), orderBy: { createdAt: "desc" }, + select: { id: true, name: true, email: true, role: true, status: true, createdAt: true } }), + prisma.user.count({ where }), + ]); + return { users, total, page: Number(page), pages: Math.ceil(total / Number(limit)) }; +} + +export async function getUserDetail(userId) { + return prisma.user.findUnique({ where: { id: Number(userId) }, include: { jobs: true, proposals: true } }); +} + +export async function updateUserStatus(userId, status) { + return prisma.user.update({ where: { id: Number(userId) }, data: { status } }); +} + +export async function listFlaggedJobs({ page = "1", limit = "20" }) { + const skip = (Number(page) - 1) * Number(limit); + const [jobs, total] = await Promise.all([ + prisma.job.findMany({ where: { isFlagged: true }, skip, take: Number(limit), + include: { user: { select: { id: true, name: true } } }, orderBy: { createdAt: "desc" } }), + prisma.job.count({ where: { isFlagged: true } }), + ]); + return { jobs, total, page: Number(page), pages: Math.ceil(total / Number(limit)) }; +} + +export async function moderateJob(jobId, action) { + const update = action === "reject" ? { isFlagged: false, status: "REJECTED" } + : action === "approve" ? { isFlagged: false } : {}; + return prisma.job.update({ where: { id: Number(jobId) }, data: update }); +} + +export async function listDisputes({ page = "1", limit = "20", status }) { + const where = {}; + if (status) where.status = status; + const skip = (Number(page) - 1) * Number(limit); + const [disputes, total] = await Promise.all([ + prisma.dispute.findMany({ where, skip, take: Number(limit), orderBy: { createdAt: "desc" } }), + prisma.dispute.count({ where }), + ]); + return { disputes, total, page: Number(page), pages: Math.ceil(total / Number(limit)) }; +} + +export async function getDisputeDetail(disputeId) { + return prisma.dispute.findUnique({ where: { id: Number(disputeId) }, include: { job: true } }); +} + +export async function resolveDispute(disputeId, ruling) { + return prisma.dispute.update({ where: { id: Number(disputeId) }, + data: { status: "RESOLVED", resolution: ruling, resolvedAt: new Date() } }); +} + +let controls = { registrationOpen: true, jobPostingOpen: true }; +export async function getPlatformControls() { return controls; } +export async function updatePlatformControls(data) { controls = { ...controls, ...data }; return controls; } From 78013640a3d06dc995a8d774991d2c03ebb93c68 Mon Sep 17 00:00:00 2001 From: qiqi1616 <760960012@qq.com> Date: Sun, 17 May 2026 22:47:20 +0800 Subject: [PATCH 2/4] feat: implement full admin panel (controllers) --- apps/api/src/controllers/adminController.js | 36 +++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/api/src/controllers/adminController.js b/apps/api/src/controllers/adminController.js index b1239568d1..0287de019f 100644 --- a/apps/api/src/controllers/adminController.js +++ b/apps/api/src/controllers/adminController.js @@ -1,6 +1,36 @@ -import { ok } from "../utils/response.js"; -import { getAdminMetrics } from "../services/adminService.js"; +import { ok, fail } from "../utils/response.js"; +import * as admin from "../services/adminService.js"; export async function metrics(req, res) { - return ok(res, await getAdminMetrics()); + try { return ok(res, await admin.getAdminMetrics()); } catch (e) { return fail(res, e.message, 500); } +} +export async function listUsers(req, res) { + try { return ok(res, await admin.listUsers(req.query)); } catch (e) { return fail(res, e.message, 500); } +} +export async function getUserDetail(req, res) { + try { return ok(res, await admin.getUserDetail(req.params.id)); } catch (e) { return fail(res, e.message, 404); } +} +export async function updateUserStatus(req, res) { + try { return ok(res, await admin.updateUserStatus(req.params.id, req.body.status)); } catch (e) { return fail(res, e.message, 500); } +} +export async function listFlaggedJobs(req, res) { + try { return ok(res, await admin.listFlaggedJobs(req.query)); } catch (e) { return fail(res, e.message, 500); } +} +export async function moderateJob(req, res) { + try { return ok(res, await admin.moderateJob(req.params.id, req.body.action)); } catch (e) { return fail(res, e.message, 500); } +} +export async function listDisputes(req, res) { + try { return ok(res, await admin.listDisputes(req.query)); } catch (e) { return fail(res, e.message, 500); } +} +export async function getDisputeDetail(req, res) { + try { return ok(res, await admin.getDisputeDetail(req.params.id)); } catch (e) { return fail(res, e.message, 404); } +} +export async function resolveDispute(req, res) { + try { return ok(res, await admin.resolveDispute(req.params.id, req.body.ruling)); } catch (e) { return fail(res, e.message, 500); } +} +export async function getControls(req, res) { + try { return ok(res, await admin.getPlatformControls()); } catch (e) { return fail(res, e.message, 500); } +} +export async function updateControls(req, res) { + try { return ok(res, await admin.updatePlatformControls(req.body)); } catch (e) { return fail(res, e.message, 500); } } From 1a6f338b8a3db325fb7b6c9a22337ec2492d3942 Mon Sep 17 00:00:00 2001 From: qiqi1616 <760960012@qq.com> Date: Sun, 17 May 2026 22:47:21 +0800 Subject: [PATCH 3/4] feat: implement full admin panel (routes) --- apps/api/src/routes/adminRoutes.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/api/src/routes/adminRoutes.js b/apps/api/src/routes/adminRoutes.js index 4c1da76f9a..3ae8345f5a 100644 --- a/apps/api/src/routes/adminRoutes.js +++ b/apps/api/src/routes/adminRoutes.js @@ -1,8 +1,22 @@ import { Router } from "express"; -import { metrics } from "../controllers/adminController.js"; +import * as ctrl from "../controllers/adminController.js"; import { authMiddleware } from "../middleware/auth.js"; export const adminRoutes = Router(); - adminRoutes.use(authMiddleware); -adminRoutes.get("/metrics", metrics); +adminRoutes.use((req, res, next) => { + if (!req.user || req.user.role !== "admin") return res.status(403).json({ error: "Admin access required" }); + next(); +}); + +adminRoutes.get("/metrics", ctrl.metrics); +adminRoutes.get("/users", ctrl.listUsers); +adminRoutes.get("/users/:id", ctrl.getUserDetail); +adminRoutes.patch("/users/:id/status", ctrl.updateUserStatus); +adminRoutes.get("/jobs/flagged", ctrl.listFlaggedJobs); +adminRoutes.post("/jobs/:id/moderate", ctrl.moderateJob); +adminRoutes.get("/disputes", ctrl.listDisputes); +adminRoutes.get("/disputes/:id", ctrl.getDisputeDetail); +adminRoutes.post("/disputes/:id/resolve", ctrl.resolveDispute); +adminRoutes.get("/controls", ctrl.getControls); +adminRoutes.put("/controls", ctrl.updateControls); From e34de61403295d5c5518081c94497926c212d1e8 Mon Sep 17 00:00:00 2001 From: qiqi1616 <760960012@qq.com> Date: Sun, 17 May 2026 22:47:23 +0800 Subject: [PATCH 4/4] feat: implement full admin panel (frontend) --- apps/web/app/admin/page.tsx | 101 +++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx index 9d251466f7..c7863802d5 100644 --- a/apps/web/app/admin/page.tsx +++ b/apps/web/app/admin/page.tsx @@ -1,8 +1,107 @@ +'use client'; +import { useState, useEffect } from 'react'; + export default function AdminPanelPage() { + const [tab, setTab] = useState('metrics'); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { fetchData(); }, [tab]); + + async function fetchData() { + setLoading(true); setError(''); + try { + const token = localStorage.getItem('token'); + const urls = { metrics: '/api/admin/metrics', users: '/api/admin/users?limit=50', flagged: '/api/admin/jobs/flagged?limit=50', disputes: '/api/admin/disputes?limit=50', controls: '/api/admin/controls' }; + const res = await fetch(urls[tab] || '/api/admin/metrics', { headers: { Authorization: 'Bearer ' + token } }); + if (res.status === 403) { setError('Admin access required — you are not an admin'); return; } + if (!res.ok) throw new Error('Failed to load'); + const json = await res.json(); + setData(json.data || json); + } catch (e) { setError(e.message); } + finally { setLoading(false); } + } + + const tabs = ['metrics', 'users', 'flagged', 'disputes', 'controls']; + return (

Admin Panel

-

Moderation queues, trust metrics, and platform controls are available here.

+

User management, moderation queues, dispute resolution, and platform controls

+ +
+ {tabs.map(t => ( + + ))} +
+ + {error &&
{error}
} + {loading &&

Loading...

} + + {!loading && !error && data && ( +
+ {tab === 'metrics' && ( +
+ {Object.entries(data).map(([k, v]) => ( +
+
{String(v)}
+
{k.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase())}
+
+ ))} +
+ )} + {tab === 'users' && data.users && ( + + + {data.users.map(u => ( + + + + + + ))} +
IDNameEmailRoleStatus
{u.id}{u.name}{u.email}{u.role}{u.status}
+ )} + {tab === 'flagged' && ( + + + {(data.jobs || []).map(j => ( + + + + + ))} +
IDTitlePosted ByActions
{j.id}{j.title}{j.user?.name || 'N/A'}FLAGGED
+ )} + {tab === 'disputes' && ( + + + {(data.disputes || []).map(d => ( + + + + + + ))} +
IDStatusCreated
{d.id}{d.status}{new Date(d.createdAt).toLocaleDateString()}
+ )} + {tab === 'controls' && ( +
+ {Object.entries(data).map(([k, v]) => ( +
+ {k.replace(/([A-Z])/g, ' $1')} + {v ? 'ENABLED' : 'DISABLED'} +
+ ))} +
+ )} +

{data.total ? data.total + ' total' : Object.keys(data).length + ' items'}

+
+ )}
); }