xpt-shared-types 1.16.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Player bans — the contract between `utils/playerBan.ts` in xpt-strapi and
3
+ * the ban dialog, ban list and host-bans page in xpt-client.
4
+ *
5
+ * Two scopes (`PlayerBanScope`, generated): **tournament** keeps a player out
6
+ * of one event and is set by its owner or an admin; **host** keeps them out
7
+ * of every tournament one host owns, now and later, and only that host sets
8
+ * it. The platform level is the account block, not a ban.
9
+ *
10
+ * A ban is active until it is lifted or its `expiresAt` passes; `null` is
11
+ * permanent. Lifted bans are kept, so a list can show history.
12
+ *
13
+ * Routes:
14
+ * - `GET|POST /tournaments/:id/bans`, `DELETE /tournaments/:id/bans/:userId`
15
+ * - `GET|POST /host-bans`, `DELETE /host-bans/:userId`
16
+ * Both GETs take `?includeLifted=true`.
17
+ */
18
+
19
+ import type { PlayerBanScope } from '../generated/enums';
20
+
21
+ export const MAX_BAN_REASON_LENGTH = 500;
22
+
23
+ /** A ban as the list and ban routes return it. */
24
+ export interface PlayerBanSummary {
25
+ documentId: string;
26
+ scope: PlayerBanScope;
27
+ reason: string | null;
28
+ createdAt: string | null;
29
+ /** Null for a permanent ban. */
30
+ expiresAt: string | null;
31
+ liftedAt: string | null;
32
+ /** Not lifted and not expired. */
33
+ active: boolean;
34
+ users_permissions_user: {
35
+ id: number;
36
+ documentId: string | null;
37
+ username: string | null;
38
+ image: unknown;
39
+ } | null;
40
+ bannedBy: { id: number; username: string | null } | null;
41
+ liftedBy: { id: number; username: string | null } | null;
42
+ /** Set on tournament-scope bans. */
43
+ tournament: { documentId: string; title: string | null } | null;
44
+ /**
45
+ * Whether the viewer may lift it. On a tournament's list a host ban is
46
+ * liftable only by the owner — an admin sees it but cannot lift it.
47
+ */
48
+ canLift: boolean;
49
+ }
50
+
51
+ /** `POST /tournaments/:id/bans` and `POST /host-bans`. */
52
+ export interface BanPlayerBody {
53
+ userId: number;
54
+ reason?: string | null;
55
+ /** ISO date in the future; omit or null for permanent. */
56
+ expiresAt?: string | null;
57
+ /**
58
+ * Also withdraw the entry the player registered (with its refund), while
59
+ * the tournament still takes entries. Host bans apply it across all of the
60
+ * host's open tournaments.
61
+ */
62
+ removeEntry?: boolean;
63
+ }
64
+
65
+ /** Why `removeEntry` left an entry where it was. */
66
+ export type BanRemovalSkip =
67
+ /** No entry in this tournament. Never returned for host bans. */
68
+ | 'notRegistered'
69
+ /** On a team someone else registered: the team's leader must swap them out. */
70
+ | 'notRegistrant'
71
+ /** Past check-in; the bracket is seeded. */
72
+ | 'entryLocked'
73
+ /** The withdrawal refused; `message` says why. */
74
+ | 'failed';
75
+
76
+ export interface BanRemoval {
77
+ tournamentDocumentId: string;
78
+ tournamentTitle: string | null;
79
+ removed: boolean;
80
+ /** Coins returned to whoever paid. */
81
+ refunded: number;
82
+ skipped?: BanRemovalSkip;
83
+ message?: string;
84
+ }
85
+
86
+ /** The `data` of a successful ban. */
87
+ export interface BanResult {
88
+ ban: PlayerBanSummary;
89
+ /** Pending join requests declined and refunded because of the ban. */
90
+ declinedRequests: number;
91
+ /** Empty unless `removeEntry` was asked for. */
92
+ removals: BanRemoval[];
93
+ }
94
+
95
+ /** The durations the ban dialog offers. */
96
+ export const BAN_DURATIONS = [
97
+ { key: '1d', label: '24 hours', days: 1 },
98
+ { key: '7d', label: '7 days', days: 7 },
99
+ { key: '30d', label: '30 days', days: 30 },
100
+ { key: 'permanent', label: 'Permanent', days: null },
101
+ ] as const;
102
+
103
+ export type BanDurationKey = (typeof BAN_DURATIONS)[number]['key'];
104
+
105
+ /** `expiresAt` for a duration, or null for permanent. */
106
+ export function banExpiryFor(
107
+ key: BanDurationKey,
108
+ now: Date = new Date()
109
+ ): string | null {
110
+ const days = BAN_DURATIONS.find((d) => d.key === key)?.days ?? null;
111
+ return days == null
112
+ ? null
113
+ : new Date(now.getTime() + days * 86_400_000).toISOString();
114
+ }
115
+
116
+ export function isBanActive(
117
+ ban: { liftedAt?: string | null; expiresAt?: string | null },
118
+ now: Date = new Date()
119
+ ): boolean {
120
+ if (ban.liftedAt) return false;
121
+ if (!ban.expiresAt) return true;
122
+ return new Date(ban.expiresAt).getTime() > now.getTime();
123
+ }
@@ -51,7 +51,9 @@ export type TournamentStaffAction =
51
51
  /** Record scores, start matches, edit match details. */
52
52
  | 'manageMatches'
53
53
  /** Watch any lobby and post as staff in its chat. */
54
- | 'enterLobby';
54
+ | 'enterLobby'
55
+ /** Ban players from this tournament and lift those bans. */
56
+ | 'manageBans';
55
57
 
56
58
  const OWNER_ONLY = ['owner'] as const;
57
59
  const ADMIN_UP = ['owner', 'admin'] as const;
@@ -72,6 +74,7 @@ export const TOURNAMENT_STAFF_CAPABILITIES: Record<
72
74
  reviewJoinRequests: ADMIN_UP,
73
75
  resolveDispute: ADMIN_UP,
74
76
  seedBracket: ADMIN_UP,
77
+ manageBans: ADMIN_UP,
75
78
  manageMatches: ALL_STAFF,
76
79
  enterLobby: ALL_STAFF,
77
80
  };