feat(admin): manual account blocking (suspensions)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
Operator-driven hard block, the counterpart to the soft high-rate flag: permanent or until a date, with an optional reason chosen from an editable en+ru picklist (snapshotted onto the block). A block forfeits the player's active games (opponent wins, as a resignation) and cancels their open matchmaking games. A backend gate refuses a blocked account on every /api/v1/user/* route except the block-status probe with 403 account_blocked, which threads through the gateway as the Execute result_code; the UI surfaces it as a terminal blocked screen and stops all push/poll. Temporary blocks self-expire; the operator can unblock at any time (lost games stay lost). Sessions are not revoked, so the blocked client can still reach the exempt block-status endpoint. Backend: migration 00003 (account_suspensions + suspension_reasons) + jet regen; account suspension store; game.ForfeitAllForAccount; requireNotSuspended gate + block-status endpoint; admin console block/unblock + Reasons CRUD. Wire: fbs BlockStatus + account.block_status gateway op. UI: blocked screen, app state, transport/codec, i18n. Docs: ARCHITECTURE, FUNCTIONAL(+ru), PRERELEASE (AB).
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
// The terminal blocked screen. While app.blocked is set, App.svelte renders only this and all
|
||||
// push/poll is stopped; the screen itself makes no network calls. The message is permanent or
|
||||
// dated (the expiry rendered in the user's locale + timezone), with the optional operator reason.
|
||||
import { app } from '../lib/app.svelte';
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
import { formatBlockUntil } from '../lib/blocked';
|
||||
|
||||
const until = $derived(
|
||||
app.blocked?.until ? formatBlockUntil(app.blocked.until, app.locale, app.profile?.timeZone ?? '') : '',
|
||||
);
|
||||
const message = $derived(until ? t('blocked.temporary', { until }) : t('blocked.permanent'));
|
||||
</script>
|
||||
|
||||
<div class="blocked">
|
||||
<div class="card">
|
||||
<h1>{t('blocked.title')}</h1>
|
||||
<p class="msg">{message}</p>
|
||||
{#if app.blocked?.reason}
|
||||
<p class="reason"><span class="label">{t('blocked.reason')}</span> {app.blocked.reason}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.blocked {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: var(--bg);
|
||||
}
|
||||
.card {
|
||||
max-width: 28rem;
|
||||
text-align: center;
|
||||
color: var(--text);
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.msg {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
.reason {
|
||||
margin: 1rem 0 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.label {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user