Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"dev": "node src/server.js",
"start": "node src/server.js",
"test": "node --test src/tests"
"test": "node --test \"src/tests/*.test.js\""
},
"dependencies": {
"cors": "^2.8.5",
Expand Down
92 changes: 91 additions & 1 deletion apps/api/src/controllers/adminController.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,96 @@
import { ok } from "../utils/response.js";
import { getAdminMetrics } from "../services/adminService.js";
import {
applyDisputeRuling,
applyModerationDecision,
getAdminMetrics,
getAdminOverview,
getAdminUserProfile,
getPlatformControls,
listAdminUsers,
listAuditLogs,
listDisputes,
listFlaggedListings,
updateAdminUserStatus,
updatePlatformControl
} from "../services/adminService.js";
import { fail } from "../utils/response.js";
import {
auditLogQuerySchema,
disputeQuerySchema,
disputeRulingSchema,
moderationDecisionSchema,
moderationQuerySchema,
platformControlSchema,
userQuerySchema,
userStatusSchema
} from "../validators/admin.js";

function adminId(req) {
return req.user?.sub ?? "unknown_admin";
}

export async function metrics(req, res) {
return ok(res, await getAdminMetrics());
}

export async function overview(req, res) {
return ok(res, await getAdminOverview());
}

export async function users(req, res) {
const query = userQuerySchema.parse(req.query);
return ok(res, await listAdminUsers(query));
}

export async function userProfile(req, res) {
const profile = await getAdminUserProfile(req.params.userId);
if (!profile) return fail(res, "User not found", 404);
return ok(res, profile);
}

export async function updateUserStatus(req, res) {
const payload = userStatusSchema.parse(req.body);
const result = await updateAdminUserStatus(req.params.userId, payload, adminId(req));
if (!result) return fail(res, "User not found", 404);
return ok(res, result);
}

export async function moderationQueue(req, res) {
const query = moderationQuerySchema.parse(req.query);
return ok(res, await listFlaggedListings(query));
}

export async function moderationDecision(req, res) {
const payload = moderationDecisionSchema.parse(req.body);
const result = await applyModerationDecision(req.params.listingId, payload, adminId(req));
if (!result) return fail(res, "Flagged listing not found", 404);
return ok(res, result);
}

export async function disputeQueue(req, res) {
const query = disputeQuerySchema.parse(req.query);
return ok(res, await listDisputes(query));
}

export async function disputeRuling(req, res) {
const payload = disputeRulingSchema.parse(req.body);
const result = await applyDisputeRuling(req.params.disputeId, payload, adminId(req));
if (!result) return fail(res, "Dispute not found", 404);
return ok(res, result);
}

export async function controls(req, res) {
return ok(res, await getPlatformControls());
}

export async function updateControl(req, res) {
const payload = platformControlSchema.parse(req.body);
const result = await updatePlatformControl(req.params.control, payload, adminId(req));
if (!result) return fail(res, "Unknown platform control", 404);
return ok(res, result);
}

export async function auditLog(req, res) {
const query = auditLogQuerySchema.parse(req.query);
return ok(res, await listAuditLogs(query));
}
8 changes: 8 additions & 0 deletions apps/api/src/middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ export function authMiddleware(req, res, next) {
return fail(res, "Invalid token", 401);
}
}

export function requireAdmin(req, res, next) {
if (req.user?.role !== "admin") {
return fail(res, "Admin access required", 403);
}

return next();
}
30 changes: 27 additions & 3 deletions apps/api/src/routes/adminRoutes.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import { Router } from "express";
import { metrics } from "../controllers/adminController.js";
import { authMiddleware } from "../middleware/auth.js";
import {
auditLog,
controls,
disputeQueue,
disputeRuling,
metrics,
moderationDecision,
moderationQueue,
overview,
updateControl,
updateUserStatus,
userProfile,
users
} from "../controllers/adminController.js";
import { authMiddleware, requireAdmin } from "../middleware/auth.js";

export const adminRoutes = Router();

adminRoutes.use(authMiddleware);
adminRoutes.use(authMiddleware, requireAdmin);
adminRoutes.get("/overview", overview);
adminRoutes.get("/metrics", metrics);
adminRoutes.get("/users", users);
adminRoutes.get("/users/:userId", userProfile);
adminRoutes.patch("/users/:userId/status", updateUserStatus);
adminRoutes.get("/moderation/jobs", moderationQueue);
adminRoutes.post("/moderation/jobs/:listingId/decision", moderationDecision);
adminRoutes.get("/disputes", disputeQueue);
adminRoutes.post("/disputes/:disputeId/ruling", disputeRuling);
adminRoutes.get("/controls", controls);
adminRoutes.patch("/controls/:control", updateControl);
adminRoutes.get("/audit-log", auditLog);
152 changes: 152 additions & 0 deletions apps/api/src/services/adminData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
export const adminUsers = [
{
id: "usr_admin_1",
email: "admin@freelanceflow.test",
fullName: "Priya Admin",
role: "admin",
status: "active",
joinedAt: "2025-11-20T10:00:00.000Z",
trustScore: 99,
activeJobs: 0,
disputeCount: 0
},
{
id: "usr_client_1",
email: "client@acme.test",
fullName: "Avery Client",
role: "client",
status: "active",
joinedAt: "2026-01-12T14:30:00.000Z",
trustScore: 82,
activeJobs: 4,
disputeCount: 1
},
{
id: "usr_client_2",
email: "ops@northstar.test",
fullName: "Northstar Ops",
role: "client",
status: "suspended",
joinedAt: "2026-03-03T09:15:00.000Z",
trustScore: 38,
activeJobs: 1,
disputeCount: 3
},
{
id: "usr_free_1",
email: "maya.dev@example.test",
fullName: "Maya Dev",
role: "freelancer",
status: "active",
joinedAt: "2025-12-08T18:45:00.000Z",
trustScore: 94,
activeJobs: 2,
disputeCount: 0
},
{
id: "usr_free_2",
email: "jordan.ux@example.test",
fullName: "Jordan UX",
role: "freelancer",
status: "active",
joinedAt: "2026-02-22T11:20:00.000Z",
trustScore: 76,
activeJobs: 3,
disputeCount: 1
},
{
id: "usr_free_3",
email: "casey.copy@example.test",
fullName: "Casey Copy",
role: "freelancer",
status: "banned",
joinedAt: "2026-04-02T16:10:00.000Z",
trustScore: 14,
activeJobs: 0,
disputeCount: 5
}
];

export const flaggedListings = [
{
id: "flag_101",
jobId: "job_101",
title: "Build payment gateway with copied credentials",
ownerId: "usr_client_2",
ownerName: "Northstar Ops",
reason: "Possible credential sharing in job brief",
status: "flagged",
riskLevel: "high",
flaggedAt: "2026-05-16T09:20:00.000Z"
},
{
id: "flag_102",
jobId: "job_102",
title: "Urgent scraper for private marketplace",
ownerId: "usr_client_1",
ownerName: "Avery Client",
reason: "Terms-of-service risk detected",
status: "flagged",
riskLevel: "medium",
flaggedAt: "2026-05-16T12:45:00.000Z"
},
{
id: "flag_103",
jobId: "job_103",
title: "Brand-safe landing page redesign",
ownerId: "usr_client_1",
ownerName: "Avery Client",
reason: "User report: unclear payment terms",
status: "escalated",
riskLevel: "low",
flaggedAt: "2026-05-15T17:05:00.000Z"
}
];

export const disputes = [
{
id: "disp_201",
jobTitle: "Migrate legacy API to Node.js",
clientId: "usr_client_1",
clientName: "Avery Client",
freelancerId: "usr_free_2",
freelancerName: "Jordan UX",
amount: 2800,
currency: "USD",
status: "open",
openedAt: "2026-05-14T13:10:00.000Z",
summary: "Client says milestone 2 is incomplete; freelancer provided deployment evidence.",
evidenceCount: 4
},
{
id: "disp_202",
jobTitle: "Design SaaS onboarding flows",
clientId: "usr_client_2",
clientName: "Northstar Ops",
freelancerId: "usr_free_1",
freelancerName: "Maya Dev",
amount: 900,
currency: "USD",
status: "under_review",
openedAt: "2026-05-12T20:25:00.000Z",
summary: "Both parties disagree on scope expansion after initial approval.",
evidenceCount: 7
}
];

export const platformControls = {
registrationsEnabled: true,
jobPostingsEnabled: true
};

export const auditLogs = [
{
id: "audit_001",
adminId: "usr_admin_1",
actionType: "platform.initialized",
targetType: "system",
targetId: "platform",
detail: "Admin panel audit log initialized",
createdAt: "2026-05-15T08:00:00.000Z"
}
];
Loading
Loading