diff --git a/dongle/app/admin/page.tsx b/dongle/app/admin/page.tsx index 11b7ad4..6490541 100644 --- a/dongle/app/admin/page.tsx +++ b/dongle/app/admin/page.tsx @@ -10,17 +10,14 @@ import WalletStatePanel, { import { useAdminAccess } from "@/hooks/useAdminAccess"; import { useConfirm } from "@/hooks/useConfirm"; import { formatDate } from "@/lib/date"; -import { - AlertCircle, Flag, Shield, CheckCircle, XCircle, Clock, MessageSquare, - CheckCheck, Archive, UserPlus, X, -} from "lucide-react"; -import { AlertCircle, Flag, Shield, CheckCircle, XCircle, Clock, MessageSquare, ScrollText } from "lucide-react"; +import { AlertCircle, Flag, Shield, CheckCircle, XCircle, Clock, MessageSquare, ScrollText, User, UserMinus } from "lucide-react"; import { reviewReportService } from "@/services/review/review-report.service"; import { projectReportService } from "@/services/project/project-report.service"; import { projectClaimService } from "@/services/project/project-claim.service"; import { projectService } from "@/services/project/project.service"; import { reviewService } from "@/services/review/review.service"; import { auditLogService } from "@/services/audit/audit-log.service"; +import { verificationService } from "@/services/stellar/verification.service"; import { ReviewReport, ModerationAction, Review } from "@/types/review"; import { ProjectReport, ProjectModerationAction, ProjectClaimRequest } from "@/types/project"; import AuditLogViewer from "@/components/admin/AuditLogViewer"; @@ -147,7 +144,8 @@ export default function AdminDashboard() { const [claimReason, setClaimReason] = useState>({}); const [projectReportReason, setProjectReportReason] = useState>({}); const [expandedReport, setExpandedReport] = useState(null); - const [reviews, setReviews] = useState([]); + const [verificationFilter, setVerificationFilter] = useState<"all" | "assigned-to-me" | "unassigned">("all"); + const [reportFilter, setReportFilter] = useState<"all" | "assigned-to-me" | "unassigned">("all"); // Load reports, reviews, and moderation log useEffect(() => { @@ -158,7 +156,6 @@ export default function AdminDashboard() { setClaimRequests(projectClaimService.getRequests()); setModerationLog(reviewReportService.getModerationLog()); setProjectModerationLog(projectReportService.getModerationLog()); - reviewService.getReviews().then(setReviews).catch(() => {}); }, 0); }, [isAdmin]); @@ -381,49 +378,81 @@ export default function AdminDashboard() { } }; - const handleResolveProjectReport = (reportId: string) => { - const reason = projectReportReason[reportId]?.trim() || "Project complies with guidelines"; + const handleAssignVerification = async (requestId: string, assignedTo: string) => { + if (!gate.publicKey) return; + + try { + await verificationService.assignRequest(requestId, gate.publicKey, assignedTo); + auditLogService.append({ + actor: gate.publicKey, + action: "verification_assigned", + targetId: requestId, + targetLabel: `Verification Request ${requestId}`, + metadata: { assignedTo }, + }); + toast.success("Request assigned"); + setRequests((prev) => prev.map((r: any) => + r.id === requestId ? { ...r, assignedTo, assignedAt: new Date().toISOString() } : r + )); + } catch (error) { + toast.error("Failed to assign request"); + } + }; + + const handleUnassignVerification = async (requestId: string) => { + if (!gate.publicKey) return; + + try { + await verificationService.unassignRequest(requestId, gate.publicKey); + auditLogService.append({ + actor: gate.publicKey, + action: "verification_unassigned", + targetId: requestId, + targetLabel: `Verification Request ${requestId}`, + }); + toast.success("Request unassigned"); + setRequests((prev) => prev.map((r: any) => + r.id === requestId ? { ...r, assignedTo: undefined, assignedAt: undefined } : r + )); + } catch (error) { + toast.error("Failed to unassign request"); + } + }; + + const handleAssignReport = (reportId: string, assignedTo: string) => { if (!gate.publicKey) return; - const result = projectReportService.resolveReport(reportId, gate.publicKey, reason); + const result = reviewReportService.assignReport(reportId, gate.publicKey, assignedTo); if (result.success) { - const report = projectReports.find((r) => r.id === reportId); auditLogService.append({ actor: gate.publicKey, - action: "report_resolved", + action: "report_assigned", targetId: reportId, - targetLabel: `Project report ${reportId}`, - reason, + targetLabel: `Report ${reportId}`, + metadata: { assignedTo }, }); - toast.success("Project report resolved"); - setProjectReports(projectReportService.getReports()); - setProjectModerationLog(projectReportService.getModerationLog()); - setProjectReportReason((prev) => ({ ...prev, [reportId]: "" })); + toast.success("Report assigned"); + setReports(reviewReportService.getReports()); } else { - toast.error(result.error || "Failed to resolve project report"); + toast.error(result.error || "Failed to assign report"); } }; - const handleDismissProjectReport = (reportId: string) => { - const reason = projectReportReason[reportId]?.trim() || "Project does not violate guidelines"; + const handleUnassignReport = (reportId: string) => { if (!gate.publicKey) return; - const result = projectReportService.dismissReport(reportId, gate.publicKey, reason); + const result = reviewReportService.unassignReport(reportId, gate.publicKey); if (result.success) { - const report = projectReports.find((r) => r.id === reportId); auditLogService.append({ actor: gate.publicKey, - action: "report_dismissed", + action: "report_unassigned", targetId: reportId, - targetLabel: `Project report ${reportId}`, - reason, + targetLabel: `Report ${reportId}`, }); - toast.success("Project report dismissed"); - setProjectReports(projectReportService.getReports()); - setProjectModerationLog(projectReportService.getModerationLog()); - setProjectReportReason((prev) => ({ ...prev, [reportId]: "" })); + toast.success("Report unassigned"); + setReports(reviewReportService.getReports()); } else { - toast.error(result.error || "Failed to dismiss project report"); + toast.error(result.error || "Failed to unassign report"); } }; @@ -548,86 +577,111 @@ export default function AdminDashboard() { Verification Requests - {selectedIds.size > 0 && ( +
+ + - )} -
- - {/* Select-all toggle */} - {requests.some((r) => r.status === "pending") && ( -
-
- )} +
- {requests.map((req) => { - const isPending = req.status === "pending"; - return ( -
-
- {isPending && ( -
- toggleSelection(req.id)} - className="w-4 h-4 rounded border-zinc-300 dark:border-zinc-700 text-purple-500 focus:ring-purple-500 cursor-pointer" - aria-label={`Select ${req.projectName}`} - /> -
+ {requests + .filter((req: any) => { + if (verificationFilter === "all") return true; + if (verificationFilter === "assigned-to-me") return req.assignedTo === gate.publicKey; + if (verificationFilter === "unassigned") return !req.assignedTo; + return true; + }) + .map((req: any) => ( +
+
+

{req.projectName}

+
+ Submitted by: + + + {formatDate(req.timestamp, "short")} +
+
+ + {req.status} + + {req.assignedTo && ( + + Assigned to + )} - -
-

{req.projectName}

-
- Submitted by: - - - {formatDate(req.timestamp, "short")} -
-
- - {req.status} - -
-
+
- {isPending && ( -
+
+ {req.assignedTo ? ( + req.assignedTo === gate.publicKey ? ( + + ) : ( + + ) + ) : ( + + )} + {req.status === "pending" && ( + <> -
+ )}
- ); - })} +
+ ))}
@@ -744,15 +798,49 @@ export default function AdminDashboard() { {activeTab === "reports" && (
-

- - Pending Reports - {(pendingReports.length + pendingProjectReports.length) > 0 && ( - - ({pendingReports.length + pendingProjectReports.length} pending) - - )} -

+
+

+ + Pending Reports + {(pendingReports.length + pendingProjectReports.length) > 0 && ( + + ({pendingReports.length + pendingProjectReports.length} pending) + + )} +

+
+ + + +
+
@@ -848,7 +936,14 @@ export default function AdminDashboard() {
) : (
- {pendingReports.map((report) => { + {pendingReports + .filter((report: any) => { + if (reportFilter === "all") return true; + if (reportFilter === "assigned-to-me") return report.assignedTo === gate.publicKey; + if (reportFilter === "unassigned") return !report.assignedTo; + return true; + }) + .map((report: any) => { const review = getReviewForReport(report.reviewId); return (
{report.reason} + {report.assignedTo && ( + + Assigned to + + )}
Reported by • {formatDate(report.createdAt, "relative")}
+
+ {report.assignedTo ? ( + report.assignedTo === gate.publicKey ? ( + + ) : ( + + ) + ) : ( + + )} +
{review && ( diff --git a/dongle/services/audit/audit-log.service.ts b/dongle/services/audit/audit-log.service.ts index a647f13..667609a 100644 --- a/dongle/services/audit/audit-log.service.ts +++ b/dongle/services/audit/audit-log.service.ts @@ -24,13 +24,13 @@ export const AUDIT_LOG_STORAGE_KEY = "dongle_audit_log"; const VALID_ACTIONS: ReadonlySet = new Set([ "verification_approved", "verification_rejected", + "verification_assigned", + "verification_unassigned", "fee_updated", "report_resolved", "report_dismissed", - "claim_approved", - "claim_rejected", - "project_flagged", - "takedown", + "report_assigned", + "report_unassigned", ]); /** Hydrate, validate, and return all stored entries (SSR-safe). */ diff --git a/dongle/services/review/review-report.service.ts b/dongle/services/review/review-report.service.ts index 7a7b917..dcfcf33 100644 --- a/dongle/services/review/review-report.service.ts +++ b/dongle/services/review/review-report.service.ts @@ -90,6 +90,8 @@ export const reviewReportService = { explanation: record.explanation, status: record.status as ReviewReportStatus, createdAt: record.createdAt, + assignedTo: typeof record.assignedTo === "string" ? record.assignedTo : undefined, + assignedAt: typeof record.assignedAt === "string" ? record.assignedAt : undefined, }; validatedReports.push(report); @@ -114,6 +116,59 @@ export const reviewReportService = { return this.getReports().filter((r) => r.status === "pending"); }, + assignReport( + reportId: string, + assignedBy: string, + assignedTo: string, + ): { success: boolean; error?: string } { + const reports = this.getReports(); + const index = reports.findIndex((r) => r.id === reportId); + + if (index === -1) { + return { success: false, error: "Report not found" }; + } + + // Update report assignment + reports[index] = { + ...reports[index], + assignedTo, + assignedAt: new Date().toISOString(), + }; + localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(reports)); + + return { success: true }; + }, + + unassignReport( + reportId: string, + unassignedBy: string, + ): { success: boolean; error?: string } { + const reports = this.getReports(); + const index = reports.findIndex((r) => r.id === reportId); + + if (index === -1) { + return { success: false, error: "Report not found" }; + } + + // Remove assignment + reports[index] = { + ...reports[index], + assignedTo: undefined, + assignedAt: undefined, + }; + localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(reports)); + + return { success: true }; + }, + + getReportsAssignedTo(adminAddress: string): ReviewReport[] { + return this.getReports().filter((r) => r.assignedTo === adminAddress); + }, + + getUnassignedReports(): ReviewReport[] { + return this.getReports().filter((r) => !r.assignedTo); + }, + hasUserReportedReview(reviewId: string, userAddress: string): boolean { return this.getReports().some( (r) => r.reviewId === reviewId && r.reporterAddress === userAddress diff --git a/dongle/services/stellar/verification.service.ts b/dongle/services/stellar/verification.service.ts index f091bcb..dffab27 100644 --- a/dongle/services/stellar/verification.service.ts +++ b/dongle/services/stellar/verification.service.ts @@ -16,6 +16,8 @@ export interface VerificationRequest { statusUpdatedAt: string; statusUpdatedBy?: string; rejectionReason?: string; + assignedTo?: string; + assignedAt?: string; } const VERIFICATION_STORAGE_KEY = "dongle_verification_requests"; @@ -126,6 +128,95 @@ class VerificationService { } } + /** + * Assigns a verification request to an admin. + */ + async assignRequest( + projectId: string, + assignedBy: string, + assignedTo: string, + ): Promise { + try { + const request = await this.getVerificationRequest(projectId); + + if (!request) { + throw new Error("Verification request not found"); + } + + // Update request assignment + request.assignedTo = assignedTo; + request.assignedAt = new Date().toISOString(); + + // Persist updated request + await this.persistRequest(request); + + console.log(`[VerificationService] Request assigned: ${projectId} to ${assignedTo}`); + return request; + } catch (error) { + console.error("[VerificationService] Error assigning request:", error); + throw error; + } + } + + /** + * Unassigns a verification request (removes assignment). + */ + async unassignRequest( + projectId: string, + unassignedBy: string, + ): Promise { + try { + const request = await this.getVerificationRequest(projectId); + + if (!request) { + throw new Error("Verification request not found"); + } + + // Remove assignment + delete request.assignedTo; + delete request.assignedAt; + + // Persist updated request + await this.persistRequest(request); + + console.log(`[VerificationService] Request unassigned: ${projectId}`); + return request; + } catch (error) { + console.error("[VerificationService] Error unassigning request:", error); + throw error; + } + } + + /** + * Gets verification requests assigned to a specific admin. + */ + async getRequestsAssignedTo(adminAddress: string): Promise { + try { + const requests = this.loadRequests(); + return requests + .filter((r) => r.assignedTo === adminAddress) + .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()); + } catch (error) { + console.error("[VerificationService] Error getting assigned requests:", error); + return []; + } + } + + /** + * Gets unassigned verification requests. + */ + async getUnassignedRequests(): Promise { + try { + const requests = this.loadRequests(); + return requests + .filter((r) => !r.assignedTo) + .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()); + } catch (error) { + console.error("[VerificationService] Error getting unassigned requests:", error); + return []; + } + } + /** * Approves a verification request (admin only). */ diff --git a/dongle/types/audit-log.ts b/dongle/types/audit-log.ts index 54dbb30..6d47b1b 100644 --- a/dongle/types/audit-log.ts +++ b/dongle/types/audit-log.ts @@ -9,25 +9,25 @@ export type AuditAction = | "verification_approved" | "verification_rejected" + | "verification_assigned" + | "verification_unassigned" | "fee_updated" | "report_resolved" | "report_dismissed" - | "claim_approved" - | "claim_rejected" - | "project_flagged" - | "takedown"; + | "report_assigned" + | "report_unassigned"; /** Human-readable labels for each action. */ export const AUDIT_ACTION_LABELS: Record = { verification_approved: "Verification Approved", verification_rejected: "Verification Rejected", + verification_assigned: "Verification Assigned", + verification_unassigned: "Verification Unassigned", fee_updated: "Fee Updated", report_resolved: "Report Resolved", report_dismissed: "Report Dismissed", - claim_approved: "Claim Approved", - claim_rejected: "Claim Rejected", - project_flagged: "Project Flagged", - takedown: "Takedown", + report_assigned: "Report Assigned", + report_unassigned: "Report Unassigned", }; /** diff --git a/dongle/types/review.ts b/dongle/types/review.ts index 318280f..b43f624 100644 --- a/dongle/types/review.ts +++ b/dongle/types/review.ts @@ -60,6 +60,8 @@ export interface ReviewReport { explanation: string; status: ReviewReportStatus; createdAt: string; + assignedTo?: string; + assignedAt?: string; } export interface ModerationAction {