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
124 changes: 112 additions & 12 deletions Front/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export default function AdminPage() {
const [authenticated, setAuthenticated] = useState(false)
const [adminKeyInput, setAdminKeyInput] = useState("")
const [loading, setLoading] = useState(false)
const [actionId, setActionId] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [notice, setNotice] = useState<string | null>(null)
const [platforms, setPlatforms] = useState<AdminPlatform[]>([])
const [lenders, setLenders] = useState<AdminLender[]>([])
const [events, setEvents] = useState<AdminEvent[]>([])
Expand Down Expand Up @@ -83,6 +85,7 @@ export default function AdminPage() {
const handleRegisterPlatform = async (event: FormEvent) => {
event.preventDefault()
setError(null)
setNotice(null)
setLastRegisteredKey(null)

const result = await adminClient.registerPlatform({
Expand All @@ -101,6 +104,59 @@ export default function AdminPage() {
loadDashboard()
}

const handleDispute = async (event: AdminEvent) => {
const reason = window.prompt("Reason for disputing this credit event?")?.trim()
if (!reason) return

setActionId(`dispute-${event.id}`)
setError(null)
setNotice(null)
const result = await adminClient.disputeEvent(event.id, reason)
setActionId(null)

if (result.error) {
setError(result.error)
return
}

setNotice("Event disputed. Recalculate the wallet to apply the score change.")
loadDashboard()
}

const handleReinstate = async (event: AdminEvent) => {
setActionId(`reinstate-${event.id}`)
setError(null)
setNotice(null)
const result = await adminClient.reinstateEvent(event.id)
setActionId(null)

if (result.error) {
setError(result.error)
return
}

setNotice("Event reinstated. Recalculate the wallet to apply the score change.")
loadDashboard()
}

const handleRecalculate = async (event: AdminEvent) => {
setActionId(`recalc-${event.id}`)
setError(null)
setNotice(null)
const result = await adminClient.recalculateUser(event.walletAddress)
setActionId(null)

if (result.error) {
setError(result.error)
return
}

setNotice(
`Recalculated ${event.walletAddress.slice(0, 8)}...: ${result.data?.previousScore} -> ${result.data?.score}`
)
loadDashboard()
}

if (!authenticated) {
return (
<DappShell withGrid={false}>
Expand Down Expand Up @@ -142,7 +198,7 @@ export default function AdminPage() {
<p className="section-label mb-2">Operator</p>
<h1 className="page-title mb-2">Admin Dashboard</h1>
<p className="page-subtitle">
Manage platforms, lenders, and monitor recent credit events.
Manage platforms, lenders, and disputed credit events.
</p>
</div>

Expand All @@ -152,6 +208,12 @@ export default function AdminPage() {
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{notice && (
<Alert className="border-white/10 bg-white/[0.03]">
<CheckCircle2 className="h-4 w-4" />
<AlertDescription>{notice}</AlertDescription>
</Alert>
)}

<Card>
<CardHeader className="flex flex-row items-center justify-between pb-3">
Expand Down Expand Up @@ -212,8 +274,12 @@ export default function AdminPage() {
</form>
{lastRegisteredKey && (
<div className="mt-4 border border-white/10 bg-white/[0.03] p-3">
<p className="text-[10px] uppercase tracking-zk-wide text-white/30 mb-1">New API key — save it now</p>
<p className="text-xs font-mono text-white/70 break-all select-all">{lastRegisteredKey}</p>
<p className="text-[10px] uppercase tracking-zk-wide text-white/30 mb-1">
New API key - save it now
</p>
<p className="text-xs font-mono text-white/70 break-all select-all">
{lastRegisteredKey}
</p>
</div>
)}
</CardContent>
Expand Down Expand Up @@ -284,6 +350,9 @@ export default function AdminPage() {
<Card>
<CardHeader>
<CardTitle>Recent events ({events.length})</CardTitle>
<CardDescription>
Dispute or reinstate an event, then recalculate that wallet.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
Expand All @@ -293,6 +362,8 @@ export default function AdminPage() {
<TableHead>Platform</TableHead>
<TableHead>Wallet</TableHead>
<TableHead>Impact</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand All @@ -305,18 +376,47 @@ export default function AdminPage() {
<TableCell className="text-xs font-mono">
{event.walletAddress.slice(0, 8)}...
</TableCell>
<TableCell
className={`text-xs tabular-nums font-bold ${
event.scoreImpact > 0
? "text-white/70"
: event.scoreImpact < 0
? "text-white/35"
: "text-white/20"
}`}
>
<TableCell className="text-xs tabular-nums font-bold">
{event.scoreImpact > 0 ? "+" : ""}
{event.scoreImpact}
</TableCell>
<TableCell className="text-xs">
{event.disputed ? (
<span title={event.disputeReason ?? undefined}>disputed</span>
) : (
"active"
)}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
{event.disputed ? (
<Button
size="sm"
variant="outline"
disabled={actionId === `reinstate-${event.id}`}
onClick={() => handleReinstate(event)}
>
Reinstate
</Button>
) : (
<Button
size="sm"
variant="outline"
disabled={actionId === `dispute-${event.id}`}
onClick={() => handleDispute(event)}
>
Dispute
</Button>
)}
<Button
size="sm"
disabled={actionId === `recalc-${event.id}`}
onClick={() => handleRecalculate(event)}
>
Recalculate
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
Expand Down
38 changes: 38 additions & 0 deletions Front/lib/admin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export interface AdminEvent {
scoreImpact: number
txHash: string
createdAt: string
disputed: boolean
disputeReason: string | null
disputedAt: string | null
disputedBy: string | null
}

export interface AdminRecalcResult {
walletAddress: string
previousScore: number
score: number
profileTier: string
stellarBase: number
eventsTotal: number
paymentsTotal: number
}

class AdminClient {
Expand Down Expand Up @@ -101,6 +115,30 @@ class AdminClient {
}>(`/api/admin/events/recent?limit=${limit}&offset=${offset}`)
}

disputeEvent(id: string, reason: string) {
return this.request<{ event: AdminEvent }>(
`/api/admin/events/${encodeURIComponent(id)}/dispute`,
{
method: "POST",
body: JSON.stringify({ reason }),
}
)
}

reinstateEvent(id: string) {
return this.request<{ event: AdminEvent }>(
`/api/admin/events/${encodeURIComponent(id)}/reinstate`,
{ method: "POST" }
)
}

recalculateUser(wallet: string) {
return this.request<AdminRecalcResult>(
`/api/admin/users/${encodeURIComponent(wallet)}/recalculate`,
{ method: "POST" }
)
}

registerPlatform(payload: {
platformId: string
name: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE `CreditEvent`
ADD COLUMN `disputed` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `disputeReason` VARCHAR(191) NULL,
ADD COLUMN `disputedAt` DATETIME(3) NULL,
ADD COLUMN `disputedBy` VARCHAR(191) NULL;
4 changes: 4 additions & 0 deletions Server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ model CreditEvent {
scoreImpact Int
verifiedAt DateTime
createdAt DateTime @default(now())
disputed Boolean @default(false)
disputeReason String?
disputedAt DateTime?
disputedBy String?
user User @relation(fields: [userId], references: [id])
platform Platform @relation(fields: [platformId], references: [id])

Expand Down
4 changes: 2 additions & 2 deletions Server/scripts/recalculate-scores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async function main() {
const users = await prisma.user.findMany({
where: options.wallet ? { walletAddress: options.wallet } : undefined,
include: {
creditEvents: { select: { scoreImpact: true } },
creditEvents: { select: { scoreImpact: true, disputed: true } },
payments: { select: { status: true } },
},
});
Expand All @@ -65,7 +65,7 @@ async function main() {
console.log(
options.apply
? "Applying score recalculation..."
: "Dry run — no database writes"
: "Dry run - no database writes"
);

for (const user of users) {
Expand Down
Loading