Add persistent cross-game player bank (Refs #146)
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { json, isHttpError } from '@sveltejs/kit';
|
||||
import { authenticateBank } from '$lib/server/auth/bank.js';
|
||||
import { bankRequest, type BankAction } from '$lib/server/fleet/bank.js';
|
||||
import { jsonBody } from '$lib/server/request-body.js';
|
||||
|
||||
export async function POST({ request, params }: { request: Request; params: { action: string } }) {
|
||||
const headers = { 'Cache-Control': 'no-store' };
|
||||
const authentication = authenticateBank(request);
|
||||
if (!authentication.ok)
|
||||
return json(
|
||||
{ ok: false, code: authentication.code },
|
||||
{ status: authentication.status, headers }
|
||||
);
|
||||
if (!['balance', 'deposit', 'withdraw', 'commit', 'cancel'].includes(params.action))
|
||||
return json({ ok: false, code: 'invalid_action' }, { status: 404, headers });
|
||||
if (
|
||||
request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !==
|
||||
'application/json'
|
||||
)
|
||||
return json({ ok: false, code: 'invalid_content_type' }, { status: 415, headers });
|
||||
let value: unknown;
|
||||
try {
|
||||
value = await jsonBody(request, 4096);
|
||||
} catch (cause) {
|
||||
if (isHttpError(cause, 413)) throw cause;
|
||||
return json({ ok: false, code: 'invalid_request' }, { status: 400, headers });
|
||||
}
|
||||
try {
|
||||
const { body, status } = bankRequest(
|
||||
params.action as BankAction,
|
||||
value,
|
||||
authentication.server
|
||||
);
|
||||
return json(body, { status, headers });
|
||||
} catch {
|
||||
console.error('Bank transaction failed; request may be retried with the same identity.');
|
||||
return json({ ok: false, code: 'unavailable' }, { status: 503, headers });
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { bankProfile } from '$lib/server/fleet/bank.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { playerReport } from '$lib/server/reports/index.js';
|
||||
import { isLeaderboardMetric } from '$lib/server/fleet/leaderboard.js';
|
||||
@@ -38,6 +39,7 @@ export async function load({ params, url }: { params: { guid: string }; url: URL
|
||||
if (!profile) error(404, 'Player not found');
|
||||
return {
|
||||
profile,
|
||||
bank: bankProfile(params.guid),
|
||||
returnTo: leaderboardReturnHref(url.searchParams.get('returnTo')),
|
||||
scope: { game: game ?? '', map: map ?? '', players: players ?? '', metric, entry }
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { bankProfile } from '$lib/server/fleet/bank.js';
|
||||
import Select from '$ui/components/primitives/Select.svelte';
|
||||
import Field from '$ui/components/primitives/Field.svelte';
|
||||
import RelativeTime from '$ui/components/RelativeTime.svelte';
|
||||
@@ -43,6 +44,7 @@
|
||||
}: {
|
||||
data: {
|
||||
profile: PlayerProfile;
|
||||
bank: ReturnType<typeof bankProfile>;
|
||||
returnTo: string;
|
||||
scope: {
|
||||
game: string;
|
||||
@@ -162,6 +164,45 @@
|
||||
description="Stats appear after a game's end report arrives, provided this player was observed at round 2 or later. Games without an end report do not contribute."
|
||||
/>
|
||||
{/if}
|
||||
<section aria-labelledby="bank-title">
|
||||
<h2 id="bank-title">Bank</h2>
|
||||
<p>
|
||||
<strong>{number.format(data.bank.balance)} points</strong> shared across Black Ops I and II.
|
||||
</p>
|
||||
{#if data.bank.pending}<p>
|
||||
A transfer is pending. Available: {number.format(data.bank.available)} points.
|
||||
</p>{/if}
|
||||
{#if data.bank.entries.length}
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<caption>Recent bank transfers</caption>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Time</th><th scope="col">Server</th><th scope="col"
|
||||
>Transfer</th
|
||||
><th scope="col">Balance</th><th scope="col">Match</th></tr
|
||||
></thead
|
||||
>
|
||||
<tbody
|
||||
>{#each data.bank.entries as entry (entry.id)}
|
||||
<tr
|
||||
><td>{$timeFormat.dateTime(entry.createdAt)}</td><td
|
||||
>{serverTitle(entry.server)}</td
|
||||
><td>{entry.amount > 0 ? '+' : ''}{number.format(entry.amount)}</td
|
||||
><td>{number.format(entry.balance)}</td><td
|
||||
>{#if entry.matchAvailable}<a href={`/matches/${entry.matchId}`}
|
||||
>#{entry.matchId}</a
|
||||
>{:else}{entry.matchId
|
||||
? `#${entry.matchId} (history expired)`
|
||||
: '—'}{/if}</td
|
||||
></tr
|
||||
>
|
||||
{/each}</tbody
|
||||
>
|
||||
</table>
|
||||
</div>
|
||||
{:else}<p>No bank transfers yet.</p>{/if}
|
||||
</section>
|
||||
<nav class="jump-links" aria-label="Player sections">
|
||||
<a href="#overview">Overview</a><a href="#maps-title">Maps</a><a href="#history-title"
|
||||
>Matches</a
|
||||
|
||||
Reference in New Issue
Block a user