From bf14b039c6621da142ad2b638245121aa89ec9ab Mon Sep 17 00:00:00 2001 From: Mike Johnston Date: Fri, 20 Mar 2026 12:24:44 -0400 Subject: [PATCH] allow an account to reset --- src/app/admin/users/AdminUserActions.tsx | 57 +++++++++++- .../api/admin/users/[userId]/reset/route.ts | 54 +++++++++++ src/app/api/user/me/reset/route.ts | 51 ++++++++++ .../profile/[username]/ResetAccountForm.tsx | 92 +++++++++++++++++++ src/app/profile/[username]/page.tsx | 2 + 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/app/api/admin/users/[userId]/reset/route.ts create mode 100644 src/app/api/user/me/reset/route.ts create mode 100644 src/app/profile/[username]/ResetAccountForm.tsx diff --git a/src/app/admin/users/AdminUserActions.tsx b/src/app/admin/users/AdminUserActions.tsx index 8fd9852..b5b26b6 100644 --- a/src/app/admin/users/AdminUserActions.tsx +++ b/src/app/admin/users/AdminUserActions.tsx @@ -26,6 +26,9 @@ export function AdminUserActions({ user }: { user: UserData }) { const [error, setError] = useState('') const [deleteConfirm, setDeleteConfirm] = useState('') const [deleteError, setDeleteError] = useState('') + const [accountResetConfirm, setAccountResetConfirm] = useState('') + const [accountResetError, setAccountResetError] = useState('') + const [accountResetDone, setAccountResetDone] = useState(false) async function handleSave() { setLoading(true) @@ -100,10 +103,29 @@ export function AdminUserActions({ user }: { user: UserData }) { } } + async function handleAccountReset() { + if (accountResetConfirm !== user.username) { + setAccountResetError('Username does not match.') + return + } + setLoading(true) + setAccountResetError('') + const res = await fetch(`/api/admin/users/${user.id}/reset`, { method: 'POST' }) + const data = await res.json() + setLoading(false) + if (!res.ok) { + setAccountResetError(data.error ?? 'Reset failed.') + } else { + setAccountResetDone(true) + setAccountResetConfirm('') + router.refresh() + } + } + return ( <> + + )} + + {/* Danger zone — delete user */}

Danger zone

diff --git a/src/app/api/admin/users/[userId]/reset/route.ts b/src/app/api/admin/users/[userId]/reset/route.ts new file mode 100644 index 0000000..2565b55 --- /dev/null +++ b/src/app/api/admin/users/[userId]/reset/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/prisma' + +const STARTING_BALANCE = 2000 + +/** + * POST /api/admin/users/[userId]/reset + * + * Admin-only. Resets a user's account: closes all positions, forfeits fund + * investments, and resets their cash balance to the default starting balance. + * Trade history is preserved. + */ +export async function POST( + _req: NextRequest, + { params }: { params: { userId: string } }, +) { + const session = await getServerSession(authOptions) + if (!session?.user.isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const user = await prisma.user.findUnique({ + where: { id: params.userId }, + select: { + isFund: true, + fundInvestments: { select: { fundId: true, shares: true } }, + }, + }) + if (!user) return NextResponse.json({ error: 'User not found.' }, { status: 404 }) + if (user.isFund) { + return NextResponse.json({ error: 'Fund accounts cannot be reset this way.' }, { status: 400 }) + } + + // Forfeit all fund investments — decrement each fund's sharesOutstanding + const fundUpdates = user.fundInvestments + .filter((inv) => inv.shares > 0) + .map((inv) => + prisma.hedgeFund.update({ + where: { id: inv.fundId }, + data: { sharesOutstanding: { decrement: inv.shares } }, + }), + ) + + await prisma.$transaction([ + ...fundUpdates, + prisma.fundInvestment.deleteMany({ where: { userId: params.userId } }), + prisma.position.updateMany({ where: { userId: params.userId }, data: { shares: 0 } }), + prisma.user.update({ where: { id: params.userId }, data: { balance: STARTING_BALANCE } }), + ]) + + return NextResponse.json({ ok: true }) +} diff --git a/src/app/api/user/me/reset/route.ts b/src/app/api/user/me/reset/route.ts new file mode 100644 index 0000000..a8ddda5 --- /dev/null +++ b/src/app/api/user/me/reset/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '@/lib/auth' +import { prisma } from '@/lib/prisma' + +const STARTING_BALANCE = 2000 + +/** + * POST /api/user/me/reset + * + * Resets the current user's account: closes all positions, forfeits fund + * investments, and resets the cash balance to the default starting balance. + * Trade history is preserved. + */ +export async function POST() { + const session = await getServerSession(authOptions) + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + + const userId = session.user.id + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + isFund: true, + fundInvestments: { select: { fundId: true, shares: true } }, + }, + }) + if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + if (user.isFund) { + return NextResponse.json({ error: 'Fund accounts cannot be reset this way.' }, { status: 400 }) + } + + // Forfeit all fund investments — decrement each fund's sharesOutstanding + const fundUpdates = user.fundInvestments + .filter((inv) => inv.shares > 0) + .map((inv) => + prisma.hedgeFund.update({ + where: { id: inv.fundId }, + data: { sharesOutstanding: { decrement: inv.shares } }, + }), + ) + + await prisma.$transaction([ + ...fundUpdates, + prisma.fundInvestment.deleteMany({ where: { userId } }), + prisma.position.updateMany({ where: { userId }, data: { shares: 0 } }), + prisma.user.update({ where: { id: userId }, data: { balance: STARTING_BALANCE } }), + ]) + + return NextResponse.json({ ok: true }) +} diff --git a/src/app/profile/[username]/ResetAccountForm.tsx b/src/app/profile/[username]/ResetAccountForm.tsx new file mode 100644 index 0000000..45a67fc --- /dev/null +++ b/src/app/profile/[username]/ResetAccountForm.tsx @@ -0,0 +1,92 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { RefreshCw } from 'lucide-react' + +interface Props { + username: string +} + +export default function ResetAccountForm({ username }: Props) { + const router = useRouter() + const [open, setOpen] = useState(false) + const [confirm, setConfirm] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [done, setDone] = useState(false) + + async function handleReset(e: React.FormEvent) { + e.preventDefault() + if (confirm !== username) { + setError('Username does not match.') + return + } + setLoading(true) + setError('') + try { + const res = await fetch('/api/user/me/reset', { method: 'POST' }) + const data = await res.json() + if (!res.ok) { + setError(data.error ?? 'Reset failed.') + return + } + setDone(true) + setOpen(false) + router.refresh() + } finally { + setLoading(false) + } + } + + return ( +
+ + + {open && ( +
+

+ This will close all your open positions, + forfeit any fund investments, and reset your cash balance back to{' '} + $2,000. Trade history is kept. +

+

+ Think of it as going bankrupt and starting over — this cannot be undone. +

+
+ + setConfirm(e.target.value)} + placeholder={username} + autoComplete="off" + className="w-full bg-surface border border-amber-500/30 focus:border-amber-500 rounded-lg px-3 py-2 text-sm focus:outline-none" + /> +
+ + {error &&

{error}

} + + +
+ )} +
+ ) +} diff --git a/src/app/profile/[username]/page.tsx b/src/app/profile/[username]/page.tsx index e6f3efa..e7c8eb0 100644 --- a/src/app/profile/[username]/page.tsx +++ b/src/app/profile/[username]/page.tsx @@ -10,6 +10,7 @@ import { TrendingUp, TrendingDown, Coins, Building2 } from 'lucide-react' import ChangePasswordForm from './ChangePasswordForm' import AccountSettingsForm from './AccountSettingsForm' import CloseAccountForm from './CloseAccountForm' +import ResetAccountForm from './ResetAccountForm' import { PriceChart } from '@/components/PriceChart' export const dynamic = 'force-dynamic' @@ -333,6 +334,7 @@ export default async function ProfilePage({ params }: Props) { currentDisplayUsername={user.displayUsername ?? null} /> + )}