allow an account to reset
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { setOpen(true); setResetUrl(null); setError(''); setDeleteConfirm(''); setDeleteError('') }}
|
||||
onClick={() => { setOpen(true); setResetUrl(null); setError(''); setDeleteConfirm(''); setDeleteError(''); setAccountResetConfirm(''); setAccountResetError(''); setAccountResetDone(false) }}
|
||||
className="text-xs text-indigo-400 hover:text-indigo-300 transition-colors"
|
||||
>
|
||||
Edit
|
||||
@@ -219,6 +241,39 @@ export function AdminUserActions({ user }: { user: UserData }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Account reset */}
|
||||
<div className="border-t border-amber-500/20 pt-4">
|
||||
<p className="text-sm text-amber-400 mb-1 font-medium">Reset account</p>
|
||||
<p className="text-xs text-slate-500 mb-3">
|
||||
Closes all positions, forfeits fund investments, and resets the balance to $2,000.
|
||||
Trade history is preserved.
|
||||
</p>
|
||||
{accountResetDone ? (
|
||||
<p className="text-xs text-emerald-400">✓ Account has been reset.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={accountResetConfirm}
|
||||
onChange={(e) => setAccountResetConfirm(e.target.value)}
|
||||
placeholder={`Type "${user.username}" to confirm`}
|
||||
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"
|
||||
/>
|
||||
{accountResetError && (
|
||||
<p className="text-red-400 text-xs">{accountResetError}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={handleAccountReset}
|
||||
disabled={loading || accountResetConfirm !== user.username}
|
||||
className="text-sm bg-amber-700/30 hover:bg-amber-700/50 text-amber-400 border border-amber-500/30 px-4 py-1.5 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Resetting…' : 'Reset account'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Danger zone — delete user */}
|
||||
<div className="border-t border-red-500/20 pt-4">
|
||||
<p className="text-sm text-red-400 mb-1 font-medium">Danger zone</p>
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 (
|
||||
<section className="bg-surface-card border border-amber-500/20 rounded-xl p-6">
|
||||
<button
|
||||
onClick={() => { setOpen((v) => !v); setError(''); setConfirm(''); setDone(false) }}
|
||||
className="flex items-center gap-2 text-sm font-medium text-amber-400 hover:text-amber-300 transition-colors"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Reset account
|
||||
{done && <span className="text-emerald-400 text-xs ml-1">✓ Reset</span>}
|
||||
<span className="ml-1 text-slate-500">{open ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<form onSubmit={handleReset} className="mt-4 space-y-4 max-w-sm">
|
||||
<p className="text-sm text-slate-400">
|
||||
This will <span className="text-amber-400 font-medium">close all your open positions</span>,
|
||||
forfeit any fund investments, and reset your cash balance back to{' '}
|
||||
<span className="text-white font-medium">$2,000</span>. Trade history is kept.
|
||||
</p>
|
||||
<p className="text-sm text-slate-400">
|
||||
Think of it as going bankrupt and starting over — this cannot be undone.
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">
|
||||
Type <span className="text-white font-mono">{username}</span> to confirm
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={confirm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || confirm !== username}
|
||||
className="flex items-center gap-2 bg-amber-700 hover:bg-amber-600 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
{loading ? 'Resetting…' : 'Reset my account'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
<ChangePasswordForm />
|
||||
<ResetAccountForm username={user.username} />
|
||||
<CloseAccountForm username={user.username} />
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user