xpt-shared-types 1.15.0 → 1.18.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.
- package/dist/generated/enums.d.ts +6 -0
- package/dist/generated/inputs.d.ts +15 -1
- package/dist/generated/models.d.ts +15 -1
- package/dist/manual/gameAccountHandle.d.ts +90 -0
- package/dist/manual/gameAccountHandle.js +203 -0
- package/dist/manual/index.d.ts +2 -0
- package/dist/manual/index.js +2 -0
- package/dist/manual/playerBan.d.ts +120 -0
- package/dist/manual/playerBan.js +45 -0
- package/dist/manual/tournamentStaff.d.ts +3 -1
- package/dist/manual/tournamentStaff.js +1 -0
- package/package.json +2 -2
- package/src/generated/enums.ts +26 -0
- package/src/generated/inputs.ts +18 -0
- package/src/generated/models.ts +18 -0
- package/src/manual/gameAccountHandle.ts +250 -0
- package/src/manual/index.ts +2 -0
- package/src/manual/playerBan.ts +123 -0
- package/src/manual/tournamentStaff.ts +4 -1
|
@@ -4,6 +4,10 @@ export type AdminActionTargetType = "user" | "tournament" | "match" | "transacti
|
|
|
4
4
|
export type FaqCategory = "gettingStarted" | "tournamentsMatches" | "hosting" | "walletCoins" | "account";
|
|
5
5
|
/** `Friendship.status` */
|
|
6
6
|
export type FriendshipStatus = "pending" | "accepted";
|
|
7
|
+
/** `GameAccount.code` */
|
|
8
|
+
export type GameAccountCode = "steam" | "psn" | "xbox" | "battlenet" | "riot" | "epic" | "ea" | "activision" | "ubisoft" | "nintendo" | "fightcade" | "apple" | "googleplay";
|
|
9
|
+
/** `GameAccount.verification` */
|
|
10
|
+
export type GameAccountVerification = "none" | "format";
|
|
7
11
|
/** `Game.genre` */
|
|
8
12
|
export type GameGenre = "FPS" | "MOBA" | "Battle Royale" | "Fighting (FGC)" | "Sports" | "Strategy/RTS" | "Sim Racing" | "Card Game" | "Retro" | "Other";
|
|
9
13
|
/** `GameRequest.customLobbies` */
|
|
@@ -28,6 +32,8 @@ export type MatchStreamPlatform = "twitch" | "youtube" | "kick" | "other";
|
|
|
28
32
|
export type MatchWinnerSlot = "home" | "away";
|
|
29
33
|
/** `Platform.code` */
|
|
30
34
|
export type PlatformCode = "pc" | "playstation" | "xbox" | "nintendo" | "mobile";
|
|
35
|
+
/** `PlayerBan.scope` */
|
|
36
|
+
export type PlayerBanScope = "tournament" | "host";
|
|
31
37
|
/** `Referral.status` */
|
|
32
38
|
export type ReferralStatus = "pending" | "completed";
|
|
33
39
|
/** `Team.current_status` */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlocksContent, MediaInput, RelationInput } from '../contracts';
|
|
2
|
-
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
2
|
+
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameAccountCode, GameAccountVerification, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, PlayerBanScope, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
3
3
|
/** Write payload for `about`. */
|
|
4
4
|
export interface AboutInput {
|
|
5
5
|
heroTitle?: string;
|
|
@@ -75,6 +75,8 @@ export interface GameInput {
|
|
|
75
75
|
/** Write payload for `game-account`. */
|
|
76
76
|
export interface GameAccountInput {
|
|
77
77
|
name?: string;
|
|
78
|
+
code?: GameAccountCode;
|
|
79
|
+
verification?: GameAccountVerification;
|
|
78
80
|
tournaments?: RelationInput | RelationInput[];
|
|
79
81
|
user_game_accounts?: RelationInput | RelationInput[];
|
|
80
82
|
imgThumb?: MediaInput;
|
|
@@ -187,6 +189,18 @@ export interface PlatformInput {
|
|
|
187
189
|
games?: RelationInput | RelationInput[];
|
|
188
190
|
game_accounts?: RelationInput | RelationInput[];
|
|
189
191
|
}
|
|
192
|
+
/** Write payload for `player-ban`. */
|
|
193
|
+
export interface PlayerBanInput {
|
|
194
|
+
scope?: PlayerBanScope;
|
|
195
|
+
tournament?: RelationInput;
|
|
196
|
+
host?: RelationInput;
|
|
197
|
+
users_permissions_user?: RelationInput;
|
|
198
|
+
bannedBy?: RelationInput;
|
|
199
|
+
reason?: string;
|
|
200
|
+
expiresAt?: string;
|
|
201
|
+
liftedAt?: string;
|
|
202
|
+
liftedBy?: RelationInput;
|
|
203
|
+
}
|
|
190
204
|
/** Write payload for `preset-avatar`. */
|
|
191
205
|
export interface PresetAvatarInput {
|
|
192
206
|
image?: MediaInput;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlocksContent, StrapiDocument, StrapiMedia, UserRole } from '../contracts';
|
|
2
|
-
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
2
|
+
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameAccountCode, GameAccountVerification, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, PlayerBanScope, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
3
3
|
/** `about` */
|
|
4
4
|
export interface About extends StrapiDocument {
|
|
5
5
|
heroTitle?: string | null;
|
|
@@ -75,6 +75,8 @@ export interface Game extends StrapiDocument {
|
|
|
75
75
|
/** `game-account` */
|
|
76
76
|
export interface GameAccount extends StrapiDocument {
|
|
77
77
|
name?: string | null;
|
|
78
|
+
code?: GameAccountCode | null;
|
|
79
|
+
verification?: GameAccountVerification | null;
|
|
78
80
|
tournaments?: Tournament[];
|
|
79
81
|
user_game_accounts?: UserGameAccount[];
|
|
80
82
|
imgThumb?: StrapiMedia | null;
|
|
@@ -187,6 +189,18 @@ export interface Platform extends StrapiDocument {
|
|
|
187
189
|
games?: Game[];
|
|
188
190
|
game_accounts?: GameAccount[];
|
|
189
191
|
}
|
|
192
|
+
/** `player-ban` */
|
|
193
|
+
export interface PlayerBan extends StrapiDocument {
|
|
194
|
+
scope?: PlayerBanScope | null;
|
|
195
|
+
tournament?: Tournament | null;
|
|
196
|
+
host?: User | null;
|
|
197
|
+
users_permissions_user?: User | null;
|
|
198
|
+
bannedBy?: User | null;
|
|
199
|
+
reason?: string | null;
|
|
200
|
+
expiresAt?: string | null;
|
|
201
|
+
liftedAt?: string | null;
|
|
202
|
+
liftedBy?: User | null;
|
|
203
|
+
}
|
|
190
204
|
/** `preset-avatar` */
|
|
191
205
|
export interface PresetAvatar extends StrapiDocument {
|
|
192
206
|
image?: StrapiMedia | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a player's handle on each account network looks like.
|
|
3
|
+
*
|
|
4
|
+
* A handle is the one thing opponents actually exchange — the PSN ID, the
|
|
5
|
+
* BattleTag, the Riot ID — so a typo in it is a match that cannot start. Until
|
|
6
|
+
* these rules existed one regex covered every network, and it happily accepted
|
|
7
|
+
* `Name#1234` for PSN (where `#` is not allowed) while rejecting nothing that a
|
|
8
|
+
* real PSN or Xbox rule would.
|
|
9
|
+
*
|
|
10
|
+
* None of this proves ownership. A network whose handles can be linked by
|
|
11
|
+
* OAuth (Steam, Battle.net, Xbox) gets a `linked` tier later; this file is the
|
|
12
|
+
* `format` tier that every network can have today. Which tier applies is a
|
|
13
|
+
* property of the catalogue row (`GameAccount.verification`), set by editors,
|
|
14
|
+
* so a rule that turns out wrong for some network can be switched off without a
|
|
15
|
+
* deploy: set the row to `none` and the generic rule applies.
|
|
16
|
+
*
|
|
17
|
+
* Lives here because the settings page validates as the player types and the
|
|
18
|
+
* backend validates again on write. Two copies of a regex is two chances for a
|
|
19
|
+
* handle to pass one and fail the other.
|
|
20
|
+
*
|
|
21
|
+
* Pure on purpose: no I/O, no profanity list (each app keeps its own), so the
|
|
22
|
+
* rules are unit-tested directly (xpt-strapi
|
|
23
|
+
* `tests/unit/utils/gameAccountHandle.test.ts`).
|
|
24
|
+
*/
|
|
25
|
+
import type { GameAccountCode, GameAccountVerification } from '../generated/enums';
|
|
26
|
+
export interface HandleRule {
|
|
27
|
+
/** The network this rule is for; absent on the generic fallback. */
|
|
28
|
+
code?: GameAccountCode;
|
|
29
|
+
/** What the field is called in copy: "PSN online ID", "BattleTag". */
|
|
30
|
+
label: string;
|
|
31
|
+
/** The field's placeholder. */
|
|
32
|
+
placeholder: string;
|
|
33
|
+
/** One sentence under the field saying what a valid value looks like. */
|
|
34
|
+
hint: string;
|
|
35
|
+
/** Applied to the normalized value. */
|
|
36
|
+
pattern: RegExp;
|
|
37
|
+
min: number;
|
|
38
|
+
max: number;
|
|
39
|
+
/**
|
|
40
|
+
* Rewrites the trimmed input before `pattern` is applied: a Steam profile
|
|
41
|
+
* URL down to its id, a bare Nintendo friend code into `SW-xxxx-xxxx-xxxx`.
|
|
42
|
+
* The result is what gets stored.
|
|
43
|
+
*/
|
|
44
|
+
normalize?: (value: string) => string;
|
|
45
|
+
}
|
|
46
|
+
export type HandleCheck = {
|
|
47
|
+
ok: true;
|
|
48
|
+
value: string;
|
|
49
|
+
} | {
|
|
50
|
+
ok: false;
|
|
51
|
+
message: string;
|
|
52
|
+
};
|
|
53
|
+
/** `error.details.name` values the game-account write endpoints raise. */
|
|
54
|
+
export type GameAccountErrorName = 'HandleInvalid' | 'HandleTaken';
|
|
55
|
+
/**
|
|
56
|
+
* The rule every network gets when it has no stricter one. Also the backstop
|
|
57
|
+
* the backend applies to every write regardless of network, so every rule
|
|
58
|
+
* below must produce a value that passes this one.
|
|
59
|
+
*
|
|
60
|
+
* Letters are `\p{L}` rather than `A-Z`: BattleTags and Riot IDs allow accented
|
|
61
|
+
* names, and a rule that rejected "Ærøn#1234" would be wrong, not strict.
|
|
62
|
+
*/
|
|
63
|
+
export declare const GENERIC_HANDLE_RULE: HandleRule;
|
|
64
|
+
/**
|
|
65
|
+
* Per-network rules. Initial values, and product decisions rather than facts:
|
|
66
|
+
* each is the documented shape as of writing, kept a little looser than the
|
|
67
|
+
* network's own signup validator so a real handle is never refused.
|
|
68
|
+
*/
|
|
69
|
+
export declare const HANDLE_RULES: Partial<Record<GameAccountCode, HandleRule>>;
|
|
70
|
+
/**
|
|
71
|
+
* The rule a network's handles are checked against: its own when the
|
|
72
|
+
* catalogue row asks for format checks and a rule exists for its code,
|
|
73
|
+
* otherwise the generic one.
|
|
74
|
+
*/
|
|
75
|
+
export declare function handleRuleFor(network: {
|
|
76
|
+
code?: GameAccountCode | string | null;
|
|
77
|
+
verification?: GameAccountVerification | string | null;
|
|
78
|
+
} | null | undefined): HandleRule;
|
|
79
|
+
/** True when the code has a rule of its own, so `format` means something. */
|
|
80
|
+
export declare function hasHandleRule(code: string | null | undefined): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Checks `raw` against `rule` and returns the value to store: trimmed, runs of
|
|
83
|
+
* whitespace collapsed to one space, then the rule's own normalization.
|
|
84
|
+
*
|
|
85
|
+
* The message is the one the player reads, so it says what a valid value looks
|
|
86
|
+
* like rather than which check failed.
|
|
87
|
+
*/
|
|
88
|
+
export declare function checkHandle(raw: string | null | undefined, rule: HandleRule): HandleCheck;
|
|
89
|
+
/** The key two handles are compared on: same network, same letters, any case. */
|
|
90
|
+
export declare function normalizeHandleKey(value: string): string;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What a player's handle on each account network looks like.
|
|
4
|
+
*
|
|
5
|
+
* A handle is the one thing opponents actually exchange — the PSN ID, the
|
|
6
|
+
* BattleTag, the Riot ID — so a typo in it is a match that cannot start. Until
|
|
7
|
+
* these rules existed one regex covered every network, and it happily accepted
|
|
8
|
+
* `Name#1234` for PSN (where `#` is not allowed) while rejecting nothing that a
|
|
9
|
+
* real PSN or Xbox rule would.
|
|
10
|
+
*
|
|
11
|
+
* None of this proves ownership. A network whose handles can be linked by
|
|
12
|
+
* OAuth (Steam, Battle.net, Xbox) gets a `linked` tier later; this file is the
|
|
13
|
+
* `format` tier that every network can have today. Which tier applies is a
|
|
14
|
+
* property of the catalogue row (`GameAccount.verification`), set by editors,
|
|
15
|
+
* so a rule that turns out wrong for some network can be switched off without a
|
|
16
|
+
* deploy: set the row to `none` and the generic rule applies.
|
|
17
|
+
*
|
|
18
|
+
* Lives here because the settings page validates as the player types and the
|
|
19
|
+
* backend validates again on write. Two copies of a regex is two chances for a
|
|
20
|
+
* handle to pass one and fail the other.
|
|
21
|
+
*
|
|
22
|
+
* Pure on purpose: no I/O, no profanity list (each app keeps its own), so the
|
|
23
|
+
* rules are unit-tested directly (xpt-strapi
|
|
24
|
+
* `tests/unit/utils/gameAccountHandle.test.ts`).
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.HANDLE_RULES = exports.GENERIC_HANDLE_RULE = void 0;
|
|
28
|
+
exports.handleRuleFor = handleRuleFor;
|
|
29
|
+
exports.hasHandleRule = hasHandleRule;
|
|
30
|
+
exports.checkHandle = checkHandle;
|
|
31
|
+
exports.normalizeHandleKey = normalizeHandleKey;
|
|
32
|
+
/**
|
|
33
|
+
* The rule every network gets when it has no stricter one. Also the backstop
|
|
34
|
+
* the backend applies to every write regardless of network, so every rule
|
|
35
|
+
* below must produce a value that passes this one.
|
|
36
|
+
*
|
|
37
|
+
* Letters are `\p{L}` rather than `A-Z`: BattleTags and Riot IDs allow accented
|
|
38
|
+
* names, and a rule that rejected "Ærøn#1234" would be wrong, not strict.
|
|
39
|
+
*/
|
|
40
|
+
exports.GENERIC_HANDLE_RULE = {
|
|
41
|
+
label: 'Game ID',
|
|
42
|
+
placeholder: 'Game ID',
|
|
43
|
+
hint: 'Enter your ID exactly as it appears in-game. Letters, numbers, spaces and _ . # - are allowed.',
|
|
44
|
+
pattern: /^[\p{L}\p{N}_ .#-]+$/u,
|
|
45
|
+
min: 3,
|
|
46
|
+
max: 32,
|
|
47
|
+
};
|
|
48
|
+
const STEAM_PROFILE_URL = /^(?:https?:\/\/)?(?:www\.)?steamcommunity\.com\/(?:id|profiles)\/([^/?#\s]+)\/?(?:[?#].*)?$/i;
|
|
49
|
+
/**
|
|
50
|
+
* Per-network rules. Initial values, and product decisions rather than facts:
|
|
51
|
+
* each is the documented shape as of writing, kept a little looser than the
|
|
52
|
+
* network's own signup validator so a real handle is never refused.
|
|
53
|
+
*/
|
|
54
|
+
exports.HANDLE_RULES = {
|
|
55
|
+
psn: {
|
|
56
|
+
code: 'psn',
|
|
57
|
+
label: 'PSN online ID',
|
|
58
|
+
placeholder: 'PSN online ID',
|
|
59
|
+
hint: '3 to 16 letters, numbers, hyphens or underscores, starting with a letter. No # tag.',
|
|
60
|
+
pattern: /^[A-Za-z][A-Za-z0-9_-]{2,15}$/,
|
|
61
|
+
min: 3,
|
|
62
|
+
max: 16,
|
|
63
|
+
},
|
|
64
|
+
xbox: {
|
|
65
|
+
code: 'xbox',
|
|
66
|
+
label: 'Gamertag',
|
|
67
|
+
placeholder: 'Gamertag or Gamertag#1234',
|
|
68
|
+
hint: 'Letters, numbers and single spaces, with the #1234 suffix if your gamertag has one.',
|
|
69
|
+
pattern: /^[A-Za-z0-9]+(?: [A-Za-z0-9]+)*(?:#\d{3,4})?$/,
|
|
70
|
+
min: 3,
|
|
71
|
+
max: 20,
|
|
72
|
+
},
|
|
73
|
+
battlenet: {
|
|
74
|
+
code: 'battlenet',
|
|
75
|
+
label: 'BattleTag',
|
|
76
|
+
placeholder: 'Name#1234',
|
|
77
|
+
hint: 'Your BattleTag including the number, e.g. Name#1234.',
|
|
78
|
+
pattern: /^\p{L}[\p{L}\p{N}]{2,11}#\d{4,7}$/u,
|
|
79
|
+
min: 8,
|
|
80
|
+
max: 20,
|
|
81
|
+
},
|
|
82
|
+
riot: {
|
|
83
|
+
code: 'riot',
|
|
84
|
+
label: 'Riot ID',
|
|
85
|
+
placeholder: 'GameName#TAG',
|
|
86
|
+
hint: 'Your Riot ID including the tagline, e.g. GameName#TAG.',
|
|
87
|
+
pattern: /^[\p{L}\p{N}](?:[\p{L}\p{N} ]{1,14}[\p{L}\p{N}])?#[\p{L}\p{N}]{3,5}$/u,
|
|
88
|
+
min: 7,
|
|
89
|
+
max: 22,
|
|
90
|
+
},
|
|
91
|
+
epic: {
|
|
92
|
+
code: 'epic',
|
|
93
|
+
label: 'Epic display name',
|
|
94
|
+
placeholder: 'Epic display name',
|
|
95
|
+
hint: '3 to 16 letters, numbers, spaces or - _ . as shown on your Epic account.',
|
|
96
|
+
pattern: /^[\p{L}\p{N}][\p{L}\p{N} ._-]{1,14}[\p{L}\p{N}._-]$/u,
|
|
97
|
+
min: 3,
|
|
98
|
+
max: 16,
|
|
99
|
+
},
|
|
100
|
+
ea: {
|
|
101
|
+
code: 'ea',
|
|
102
|
+
label: 'EA ID',
|
|
103
|
+
placeholder: 'EA ID',
|
|
104
|
+
hint: '4 to 16 letters, numbers, hyphens or underscores.',
|
|
105
|
+
pattern: /^[A-Za-z0-9_-]{4,16}$/,
|
|
106
|
+
min: 4,
|
|
107
|
+
max: 16,
|
|
108
|
+
},
|
|
109
|
+
activision: {
|
|
110
|
+
code: 'activision',
|
|
111
|
+
label: 'Activision ID',
|
|
112
|
+
placeholder: 'Name#1234567',
|
|
113
|
+
hint: 'Your Activision ID including the number, e.g. Name#1234567.',
|
|
114
|
+
pattern: /^[A-Za-z0-9 _-]{2,16}#\d{4,8}$/,
|
|
115
|
+
min: 7,
|
|
116
|
+
max: 25,
|
|
117
|
+
},
|
|
118
|
+
ubisoft: {
|
|
119
|
+
code: 'ubisoft',
|
|
120
|
+
label: 'Ubisoft username',
|
|
121
|
+
placeholder: 'Ubisoft username',
|
|
122
|
+
hint: '3 to 15 letters, numbers or - _ . as shown in Ubisoft Connect.',
|
|
123
|
+
pattern: /^[A-Za-z0-9._-]{3,15}$/,
|
|
124
|
+
min: 3,
|
|
125
|
+
max: 15,
|
|
126
|
+
},
|
|
127
|
+
steam: {
|
|
128
|
+
code: 'steam',
|
|
129
|
+
label: 'Steam ID',
|
|
130
|
+
placeholder: 'Friend code or profile URL',
|
|
131
|
+
hint: 'Your Steam friend code (Friends > Add a Friend), your SteamID64, or your steamcommunity.com profile URL.',
|
|
132
|
+
// A profile URL is the easiest thing for a player to paste; what gets
|
|
133
|
+
// stored is the id or custom name at the end of it.
|
|
134
|
+
normalize: (value) => value.replace(STEAM_PROFILE_URL, '$1'),
|
|
135
|
+
pattern: /^[A-Za-z0-9_-]{3,32}$/,
|
|
136
|
+
min: 3,
|
|
137
|
+
max: 32,
|
|
138
|
+
},
|
|
139
|
+
nintendo: {
|
|
140
|
+
code: 'nintendo',
|
|
141
|
+
label: 'Nintendo friend code',
|
|
142
|
+
placeholder: 'SW-0000-0000-0000',
|
|
143
|
+
hint: 'Your 12-digit Switch friend code, e.g. SW-1234-5678-9012.',
|
|
144
|
+
normalize: (value) => {
|
|
145
|
+
const digits = value.replace(/^sw/i, '').replace(/[\s-]/g, '');
|
|
146
|
+
return /^\d{12}$/.test(digits)
|
|
147
|
+
? `SW-${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8)}`
|
|
148
|
+
: value;
|
|
149
|
+
},
|
|
150
|
+
pattern: /^SW-\d{4}-\d{4}-\d{4}$/,
|
|
151
|
+
min: 17,
|
|
152
|
+
max: 17,
|
|
153
|
+
},
|
|
154
|
+
// fightcade, apple and googleplay have no documented handle shape and stay
|
|
155
|
+
// on the generic rule.
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* The rule a network's handles are checked against: its own when the
|
|
159
|
+
* catalogue row asks for format checks and a rule exists for its code,
|
|
160
|
+
* otherwise the generic one.
|
|
161
|
+
*/
|
|
162
|
+
function handleRuleFor(network) {
|
|
163
|
+
var _a;
|
|
164
|
+
if ((network === null || network === void 0 ? void 0 : network.verification) !== 'format' || !network.code) {
|
|
165
|
+
return exports.GENERIC_HANDLE_RULE;
|
|
166
|
+
}
|
|
167
|
+
return (_a = exports.HANDLE_RULES[network.code]) !== null && _a !== void 0 ? _a : exports.GENERIC_HANDLE_RULE;
|
|
168
|
+
}
|
|
169
|
+
/** True when the code has a rule of its own, so `format` means something. */
|
|
170
|
+
function hasHandleRule(code) {
|
|
171
|
+
return !!code && Object.prototype.hasOwnProperty.call(exports.HANDLE_RULES, code);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Checks `raw` against `rule` and returns the value to store: trimmed, runs of
|
|
175
|
+
* whitespace collapsed to one space, then the rule's own normalization.
|
|
176
|
+
*
|
|
177
|
+
* The message is the one the player reads, so it says what a valid value looks
|
|
178
|
+
* like rather than which check failed.
|
|
179
|
+
*/
|
|
180
|
+
function checkHandle(raw, rule) {
|
|
181
|
+
const collapsed = (raw !== null && raw !== void 0 ? raw : '').trim().replace(/\s+/g, ' ');
|
|
182
|
+
if (!collapsed) {
|
|
183
|
+
return { ok: false, message: `Please enter your ${rule.label}` };
|
|
184
|
+
}
|
|
185
|
+
const value = rule.normalize ? rule.normalize(collapsed) : collapsed;
|
|
186
|
+
if (value.length < rule.min) {
|
|
187
|
+
return { ok: false, message: `${rule.label} is too short. ${rule.hint}` };
|
|
188
|
+
}
|
|
189
|
+
if (value.length > rule.max) {
|
|
190
|
+
return { ok: false, message: `${rule.label} is too long. ${rule.hint}` };
|
|
191
|
+
}
|
|
192
|
+
if (!rule.pattern.test(value)) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
message: `That doesn't look like a valid ${rule.label}. ${rule.hint}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { ok: true, value };
|
|
199
|
+
}
|
|
200
|
+
/** The key two handles are compared on: same network, same letters, any case. */
|
|
201
|
+
function normalizeHandleKey(value) {
|
|
202
|
+
return value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
203
|
+
}
|
package/dist/manual/index.d.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Hand-written types that are not derived from a Strapi content type.
|
|
3
3
|
*/
|
|
4
4
|
export * from './country';
|
|
5
|
+
export * from './gameAccountHandle';
|
|
5
6
|
export * from './lobby';
|
|
6
7
|
export * from './notification';
|
|
8
|
+
export * from './playerBan';
|
|
7
9
|
export * from './tournamentEntry';
|
|
8
10
|
export * from './tournamentInvite';
|
|
9
11
|
export * from './tournamentJoinRequest';
|
package/dist/manual/index.js
CHANGED
|
@@ -18,8 +18,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
* Hand-written types that are not derived from a Strapi content type.
|
|
19
19
|
*/
|
|
20
20
|
__exportStar(require("./country"), exports);
|
|
21
|
+
__exportStar(require("./gameAccountHandle"), exports);
|
|
21
22
|
__exportStar(require("./lobby"), exports);
|
|
22
23
|
__exportStar(require("./notification"), exports);
|
|
24
|
+
__exportStar(require("./playerBan"), exports);
|
|
23
25
|
__exportStar(require("./tournamentEntry"), exports);
|
|
24
26
|
__exportStar(require("./tournamentInvite"), exports);
|
|
25
27
|
__exportStar(require("./tournamentJoinRequest"), exports);
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
import type { PlayerBanScope } from '../generated/enums';
|
|
19
|
+
export declare const MAX_BAN_REASON_LENGTH = 500;
|
|
20
|
+
/** A ban as the list and ban routes return it. */
|
|
21
|
+
export interface PlayerBanSummary {
|
|
22
|
+
documentId: string;
|
|
23
|
+
scope: PlayerBanScope;
|
|
24
|
+
reason: string | null;
|
|
25
|
+
createdAt: string | null;
|
|
26
|
+
/** Null for a permanent ban. */
|
|
27
|
+
expiresAt: string | null;
|
|
28
|
+
liftedAt: string | null;
|
|
29
|
+
/** Not lifted and not expired. */
|
|
30
|
+
active: boolean;
|
|
31
|
+
users_permissions_user: {
|
|
32
|
+
id: number;
|
|
33
|
+
documentId: string | null;
|
|
34
|
+
username: string | null;
|
|
35
|
+
image: unknown;
|
|
36
|
+
} | null;
|
|
37
|
+
bannedBy: {
|
|
38
|
+
id: number;
|
|
39
|
+
username: string | null;
|
|
40
|
+
} | null;
|
|
41
|
+
liftedBy: {
|
|
42
|
+
id: number;
|
|
43
|
+
username: string | null;
|
|
44
|
+
} | null;
|
|
45
|
+
/** Set on tournament-scope bans. */
|
|
46
|
+
tournament: {
|
|
47
|
+
documentId: string;
|
|
48
|
+
title: string | null;
|
|
49
|
+
} | null;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the viewer may lift it. On a tournament's list a host ban is
|
|
52
|
+
* liftable only by the owner — an admin sees it but cannot lift it.
|
|
53
|
+
*/
|
|
54
|
+
canLift: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** `POST /tournaments/:id/bans` and `POST /host-bans`. */
|
|
57
|
+
export interface BanPlayerBody {
|
|
58
|
+
userId: number;
|
|
59
|
+
reason?: string | null;
|
|
60
|
+
/** ISO date in the future; omit or null for permanent. */
|
|
61
|
+
expiresAt?: string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Also withdraw the entry the player registered (with its refund), while
|
|
64
|
+
* the tournament still takes entries. Host bans apply it across all of the
|
|
65
|
+
* host's open tournaments.
|
|
66
|
+
*/
|
|
67
|
+
removeEntry?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** Why `removeEntry` left an entry where it was. */
|
|
70
|
+
export type BanRemovalSkip =
|
|
71
|
+
/** No entry in this tournament. Never returned for host bans. */
|
|
72
|
+
'notRegistered'
|
|
73
|
+
/** On a team someone else registered: the team's leader must swap them out. */
|
|
74
|
+
| 'notRegistrant'
|
|
75
|
+
/** Past check-in; the bracket is seeded. */
|
|
76
|
+
| 'entryLocked'
|
|
77
|
+
/** The withdrawal refused; `message` says why. */
|
|
78
|
+
| 'failed';
|
|
79
|
+
export interface BanRemoval {
|
|
80
|
+
tournamentDocumentId: string;
|
|
81
|
+
tournamentTitle: string | null;
|
|
82
|
+
removed: boolean;
|
|
83
|
+
/** Coins returned to whoever paid. */
|
|
84
|
+
refunded: number;
|
|
85
|
+
skipped?: BanRemovalSkip;
|
|
86
|
+
message?: string;
|
|
87
|
+
}
|
|
88
|
+
/** The `data` of a successful ban. */
|
|
89
|
+
export interface BanResult {
|
|
90
|
+
ban: PlayerBanSummary;
|
|
91
|
+
/** Pending join requests declined and refunded because of the ban. */
|
|
92
|
+
declinedRequests: number;
|
|
93
|
+
/** Empty unless `removeEntry` was asked for. */
|
|
94
|
+
removals: BanRemoval[];
|
|
95
|
+
}
|
|
96
|
+
/** The durations the ban dialog offers. */
|
|
97
|
+
export declare const BAN_DURATIONS: readonly [{
|
|
98
|
+
readonly key: "1d";
|
|
99
|
+
readonly label: "24 hours";
|
|
100
|
+
readonly days: 1;
|
|
101
|
+
}, {
|
|
102
|
+
readonly key: "7d";
|
|
103
|
+
readonly label: "7 days";
|
|
104
|
+
readonly days: 7;
|
|
105
|
+
}, {
|
|
106
|
+
readonly key: "30d";
|
|
107
|
+
readonly label: "30 days";
|
|
108
|
+
readonly days: 30;
|
|
109
|
+
}, {
|
|
110
|
+
readonly key: "permanent";
|
|
111
|
+
readonly label: "Permanent";
|
|
112
|
+
readonly days: null;
|
|
113
|
+
}];
|
|
114
|
+
export type BanDurationKey = (typeof BAN_DURATIONS)[number]['key'];
|
|
115
|
+
/** `expiresAt` for a duration, or null for permanent. */
|
|
116
|
+
export declare function banExpiryFor(key: BanDurationKey, now?: Date): string | null;
|
|
117
|
+
export declare function isBanActive(ban: {
|
|
118
|
+
liftedAt?: string | null;
|
|
119
|
+
expiresAt?: string | null;
|
|
120
|
+
}, now?: Date): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Player bans — the contract between `utils/playerBan.ts` in xpt-strapi and
|
|
4
|
+
* the ban dialog, ban list and host-bans page in xpt-client.
|
|
5
|
+
*
|
|
6
|
+
* Two scopes (`PlayerBanScope`, generated): **tournament** keeps a player out
|
|
7
|
+
* of one event and is set by its owner or an admin; **host** keeps them out
|
|
8
|
+
* of every tournament one host owns, now and later, and only that host sets
|
|
9
|
+
* it. The platform level is the account block, not a ban.
|
|
10
|
+
*
|
|
11
|
+
* A ban is active until it is lifted or its `expiresAt` passes; `null` is
|
|
12
|
+
* permanent. Lifted bans are kept, so a list can show history.
|
|
13
|
+
*
|
|
14
|
+
* Routes:
|
|
15
|
+
* - `GET|POST /tournaments/:id/bans`, `DELETE /tournaments/:id/bans/:userId`
|
|
16
|
+
* - `GET|POST /host-bans`, `DELETE /host-bans/:userId`
|
|
17
|
+
* Both GETs take `?includeLifted=true`.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.BAN_DURATIONS = exports.MAX_BAN_REASON_LENGTH = void 0;
|
|
21
|
+
exports.banExpiryFor = banExpiryFor;
|
|
22
|
+
exports.isBanActive = isBanActive;
|
|
23
|
+
exports.MAX_BAN_REASON_LENGTH = 500;
|
|
24
|
+
/** The durations the ban dialog offers. */
|
|
25
|
+
exports.BAN_DURATIONS = [
|
|
26
|
+
{ key: '1d', label: '24 hours', days: 1 },
|
|
27
|
+
{ key: '7d', label: '7 days', days: 7 },
|
|
28
|
+
{ key: '30d', label: '30 days', days: 30 },
|
|
29
|
+
{ key: 'permanent', label: 'Permanent', days: null },
|
|
30
|
+
];
|
|
31
|
+
/** `expiresAt` for a duration, or null for permanent. */
|
|
32
|
+
function banExpiryFor(key, now = new Date()) {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const days = (_b = (_a = exports.BAN_DURATIONS.find((d) => d.key === key)) === null || _a === void 0 ? void 0 : _a.days) !== null && _b !== void 0 ? _b : null;
|
|
35
|
+
return days == null
|
|
36
|
+
? null
|
|
37
|
+
: new Date(now.getTime() + days * 86400000).toISOString();
|
|
38
|
+
}
|
|
39
|
+
function isBanActive(ban, now = new Date()) {
|
|
40
|
+
if (ban.liftedAt)
|
|
41
|
+
return false;
|
|
42
|
+
if (!ban.expiresAt)
|
|
43
|
+
return true;
|
|
44
|
+
return new Date(ban.expiresAt).getTime() > now.getTime();
|
|
45
|
+
}
|
|
@@ -36,7 +36,9 @@ export type TournamentStaffAction =
|
|
|
36
36
|
/** Record scores, start matches, edit match details. */
|
|
37
37
|
| 'manageMatches'
|
|
38
38
|
/** Watch any lobby and post as staff in its chat. */
|
|
39
|
-
| 'enterLobby'
|
|
39
|
+
| 'enterLobby'
|
|
40
|
+
/** Ban players from this tournament and lift those bans. */
|
|
41
|
+
| 'manageBans';
|
|
40
42
|
export declare const TOURNAMENT_STAFF_CAPABILITIES: Record<TournamentStaffAction, readonly TournamentStaffRole[]>;
|
|
41
43
|
export declare const TOURNAMENT_STAFF_ROLE_LABELS: Record<TournamentStaffRole, string>;
|
|
42
44
|
export declare const tournamentStaffRoleLabel: (role: string | null | undefined) => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xpt-shared-types",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Shared types and data for XPT projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "https://github.com/otemu/xpt-shared-types"
|
|
19
|
+
"url": "git+https://github.com/otemu/xpt-shared-types.git"
|
|
20
20
|
},
|
|
21
21
|
"author": "",
|
|
22
22
|
"license": "MIT",
|
package/src/generated/enums.ts
CHANGED
|
@@ -22,6 +22,27 @@ export type FriendshipStatus =
|
|
|
22
22
|
| "pending"
|
|
23
23
|
| "accepted";
|
|
24
24
|
|
|
25
|
+
/** `GameAccount.code` */
|
|
26
|
+
export type GameAccountCode =
|
|
27
|
+
| "steam"
|
|
28
|
+
| "psn"
|
|
29
|
+
| "xbox"
|
|
30
|
+
| "battlenet"
|
|
31
|
+
| "riot"
|
|
32
|
+
| "epic"
|
|
33
|
+
| "ea"
|
|
34
|
+
| "activision"
|
|
35
|
+
| "ubisoft"
|
|
36
|
+
| "nintendo"
|
|
37
|
+
| "fightcade"
|
|
38
|
+
| "apple"
|
|
39
|
+
| "googleplay";
|
|
40
|
+
|
|
41
|
+
/** `GameAccount.verification` */
|
|
42
|
+
export type GameAccountVerification =
|
|
43
|
+
| "none"
|
|
44
|
+
| "format";
|
|
45
|
+
|
|
25
46
|
/** `Game.genre` */
|
|
26
47
|
export type GameGenre =
|
|
27
48
|
| "FPS"
|
|
@@ -126,6 +147,11 @@ export type PlatformCode =
|
|
|
126
147
|
| "nintendo"
|
|
127
148
|
| "mobile";
|
|
128
149
|
|
|
150
|
+
/** `PlayerBan.scope` */
|
|
151
|
+
export type PlayerBanScope =
|
|
152
|
+
| "tournament"
|
|
153
|
+
| "host";
|
|
154
|
+
|
|
129
155
|
/** `Referral.status` */
|
|
130
156
|
export type ReferralStatus =
|
|
131
157
|
| "pending"
|
package/src/generated/inputs.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
AdminActionTargetType,
|
|
12
12
|
FaqCategory,
|
|
13
13
|
FriendshipStatus,
|
|
14
|
+
GameAccountCode,
|
|
15
|
+
GameAccountVerification,
|
|
14
16
|
GameGenre,
|
|
15
17
|
GameRequestCustomLobbies,
|
|
16
18
|
GameRequestGenre,
|
|
@@ -23,6 +25,7 @@ import type {
|
|
|
23
25
|
MatchStreamPlatform,
|
|
24
26
|
MatchWinnerSlot,
|
|
25
27
|
PlatformCode,
|
|
28
|
+
PlayerBanScope,
|
|
26
29
|
ReferralStatus,
|
|
27
30
|
TeamCurrentStatus,
|
|
28
31
|
TeamInviteStatus,
|
|
@@ -126,6 +129,8 @@ export interface GameInput {
|
|
|
126
129
|
/** Write payload for `game-account`. */
|
|
127
130
|
export interface GameAccountInput {
|
|
128
131
|
name?: string;
|
|
132
|
+
code?: GameAccountCode;
|
|
133
|
+
verification?: GameAccountVerification;
|
|
129
134
|
tournaments?: RelationInput | RelationInput[];
|
|
130
135
|
user_game_accounts?: RelationInput | RelationInput[];
|
|
131
136
|
imgThumb?: MediaInput;
|
|
@@ -247,6 +252,19 @@ export interface PlatformInput {
|
|
|
247
252
|
game_accounts?: RelationInput | RelationInput[];
|
|
248
253
|
}
|
|
249
254
|
|
|
255
|
+
/** Write payload for `player-ban`. */
|
|
256
|
+
export interface PlayerBanInput {
|
|
257
|
+
scope?: PlayerBanScope;
|
|
258
|
+
tournament?: RelationInput;
|
|
259
|
+
host?: RelationInput;
|
|
260
|
+
users_permissions_user?: RelationInput;
|
|
261
|
+
bannedBy?: RelationInput;
|
|
262
|
+
reason?: string;
|
|
263
|
+
expiresAt?: string;
|
|
264
|
+
liftedAt?: string;
|
|
265
|
+
liftedBy?: RelationInput;
|
|
266
|
+
}
|
|
267
|
+
|
|
250
268
|
/** Write payload for `preset-avatar`. */
|
|
251
269
|
export interface PresetAvatarInput {
|
|
252
270
|
image?: MediaInput;
|
package/src/generated/models.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
AdminActionTargetType,
|
|
13
13
|
FaqCategory,
|
|
14
14
|
FriendshipStatus,
|
|
15
|
+
GameAccountCode,
|
|
16
|
+
GameAccountVerification,
|
|
15
17
|
GameGenre,
|
|
16
18
|
GameRequestCustomLobbies,
|
|
17
19
|
GameRequestGenre,
|
|
@@ -24,6 +26,7 @@ import type {
|
|
|
24
26
|
MatchStreamPlatform,
|
|
25
27
|
MatchWinnerSlot,
|
|
26
28
|
PlatformCode,
|
|
29
|
+
PlayerBanScope,
|
|
27
30
|
ReferralStatus,
|
|
28
31
|
TeamCurrentStatus,
|
|
29
32
|
TeamInviteStatus,
|
|
@@ -127,6 +130,8 @@ export interface Game extends StrapiDocument {
|
|
|
127
130
|
/** `game-account` */
|
|
128
131
|
export interface GameAccount extends StrapiDocument {
|
|
129
132
|
name?: string | null;
|
|
133
|
+
code?: GameAccountCode | null;
|
|
134
|
+
verification?: GameAccountVerification | null;
|
|
130
135
|
tournaments?: Tournament[];
|
|
131
136
|
user_game_accounts?: UserGameAccount[];
|
|
132
137
|
imgThumb?: StrapiMedia | null;
|
|
@@ -248,6 +253,19 @@ export interface Platform extends StrapiDocument {
|
|
|
248
253
|
game_accounts?: GameAccount[];
|
|
249
254
|
}
|
|
250
255
|
|
|
256
|
+
/** `player-ban` */
|
|
257
|
+
export interface PlayerBan extends StrapiDocument {
|
|
258
|
+
scope?: PlayerBanScope | null;
|
|
259
|
+
tournament?: Tournament | null;
|
|
260
|
+
host?: User | null;
|
|
261
|
+
users_permissions_user?: User | null;
|
|
262
|
+
bannedBy?: User | null;
|
|
263
|
+
reason?: string | null;
|
|
264
|
+
expiresAt?: string | null;
|
|
265
|
+
liftedAt?: string | null;
|
|
266
|
+
liftedBy?: User | null;
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
/** `preset-avatar` */
|
|
252
270
|
export interface PresetAvatar extends StrapiDocument {
|
|
253
271
|
image?: StrapiMedia | null;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a player's handle on each account network looks like.
|
|
3
|
+
*
|
|
4
|
+
* A handle is the one thing opponents actually exchange — the PSN ID, the
|
|
5
|
+
* BattleTag, the Riot ID — so a typo in it is a match that cannot start. Until
|
|
6
|
+
* these rules existed one regex covered every network, and it happily accepted
|
|
7
|
+
* `Name#1234` for PSN (where `#` is not allowed) while rejecting nothing that a
|
|
8
|
+
* real PSN or Xbox rule would.
|
|
9
|
+
*
|
|
10
|
+
* None of this proves ownership. A network whose handles can be linked by
|
|
11
|
+
* OAuth (Steam, Battle.net, Xbox) gets a `linked` tier later; this file is the
|
|
12
|
+
* `format` tier that every network can have today. Which tier applies is a
|
|
13
|
+
* property of the catalogue row (`GameAccount.verification`), set by editors,
|
|
14
|
+
* so a rule that turns out wrong for some network can be switched off without a
|
|
15
|
+
* deploy: set the row to `none` and the generic rule applies.
|
|
16
|
+
*
|
|
17
|
+
* Lives here because the settings page validates as the player types and the
|
|
18
|
+
* backend validates again on write. Two copies of a regex is two chances for a
|
|
19
|
+
* handle to pass one and fail the other.
|
|
20
|
+
*
|
|
21
|
+
* Pure on purpose: no I/O, no profanity list (each app keeps its own), so the
|
|
22
|
+
* rules are unit-tested directly (xpt-strapi
|
|
23
|
+
* `tests/unit/utils/gameAccountHandle.test.ts`).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
GameAccountCode,
|
|
28
|
+
GameAccountVerification,
|
|
29
|
+
} from '../generated/enums';
|
|
30
|
+
|
|
31
|
+
export interface HandleRule {
|
|
32
|
+
/** The network this rule is for; absent on the generic fallback. */
|
|
33
|
+
code?: GameAccountCode;
|
|
34
|
+
/** What the field is called in copy: "PSN online ID", "BattleTag". */
|
|
35
|
+
label: string;
|
|
36
|
+
/** The field's placeholder. */
|
|
37
|
+
placeholder: string;
|
|
38
|
+
/** One sentence under the field saying what a valid value looks like. */
|
|
39
|
+
hint: string;
|
|
40
|
+
/** Applied to the normalized value. */
|
|
41
|
+
pattern: RegExp;
|
|
42
|
+
min: number;
|
|
43
|
+
max: number;
|
|
44
|
+
/**
|
|
45
|
+
* Rewrites the trimmed input before `pattern` is applied: a Steam profile
|
|
46
|
+
* URL down to its id, a bare Nintendo friend code into `SW-xxxx-xxxx-xxxx`.
|
|
47
|
+
* The result is what gets stored.
|
|
48
|
+
*/
|
|
49
|
+
normalize?: (value: string) => string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type HandleCheck =
|
|
53
|
+
| { ok: true; value: string }
|
|
54
|
+
| { ok: false; message: string };
|
|
55
|
+
|
|
56
|
+
/** `error.details.name` values the game-account write endpoints raise. */
|
|
57
|
+
export type GameAccountErrorName = 'HandleInvalid' | 'HandleTaken';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The rule every network gets when it has no stricter one. Also the backstop
|
|
61
|
+
* the backend applies to every write regardless of network, so every rule
|
|
62
|
+
* below must produce a value that passes this one.
|
|
63
|
+
*
|
|
64
|
+
* Letters are `\p{L}` rather than `A-Z`: BattleTags and Riot IDs allow accented
|
|
65
|
+
* names, and a rule that rejected "Ærøn#1234" would be wrong, not strict.
|
|
66
|
+
*/
|
|
67
|
+
export const GENERIC_HANDLE_RULE: HandleRule = {
|
|
68
|
+
label: 'Game ID',
|
|
69
|
+
placeholder: 'Game ID',
|
|
70
|
+
hint: 'Enter your ID exactly as it appears in-game. Letters, numbers, spaces and _ . # - are allowed.',
|
|
71
|
+
pattern: /^[\p{L}\p{N}_ .#-]+$/u,
|
|
72
|
+
min: 3,
|
|
73
|
+
max: 32,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const STEAM_PROFILE_URL =
|
|
77
|
+
/^(?:https?:\/\/)?(?:www\.)?steamcommunity\.com\/(?:id|profiles)\/([^/?#\s]+)\/?(?:[?#].*)?$/i;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Per-network rules. Initial values, and product decisions rather than facts:
|
|
81
|
+
* each is the documented shape as of writing, kept a little looser than the
|
|
82
|
+
* network's own signup validator so a real handle is never refused.
|
|
83
|
+
*/
|
|
84
|
+
export const HANDLE_RULES: Partial<Record<GameAccountCode, HandleRule>> = {
|
|
85
|
+
psn: {
|
|
86
|
+
code: 'psn',
|
|
87
|
+
label: 'PSN online ID',
|
|
88
|
+
placeholder: 'PSN online ID',
|
|
89
|
+
hint: '3 to 16 letters, numbers, hyphens or underscores, starting with a letter. No # tag.',
|
|
90
|
+
pattern: /^[A-Za-z][A-Za-z0-9_-]{2,15}$/,
|
|
91
|
+
min: 3,
|
|
92
|
+
max: 16,
|
|
93
|
+
},
|
|
94
|
+
xbox: {
|
|
95
|
+
code: 'xbox',
|
|
96
|
+
label: 'Gamertag',
|
|
97
|
+
placeholder: 'Gamertag or Gamertag#1234',
|
|
98
|
+
hint: 'Letters, numbers and single spaces, with the #1234 suffix if your gamertag has one.',
|
|
99
|
+
pattern: /^[A-Za-z0-9]+(?: [A-Za-z0-9]+)*(?:#\d{3,4})?$/,
|
|
100
|
+
min: 3,
|
|
101
|
+
max: 20,
|
|
102
|
+
},
|
|
103
|
+
battlenet: {
|
|
104
|
+
code: 'battlenet',
|
|
105
|
+
label: 'BattleTag',
|
|
106
|
+
placeholder: 'Name#1234',
|
|
107
|
+
hint: 'Your BattleTag including the number, e.g. Name#1234.',
|
|
108
|
+
pattern: /^\p{L}[\p{L}\p{N}]{2,11}#\d{4,7}$/u,
|
|
109
|
+
min: 8,
|
|
110
|
+
max: 20,
|
|
111
|
+
},
|
|
112
|
+
riot: {
|
|
113
|
+
code: 'riot',
|
|
114
|
+
label: 'Riot ID',
|
|
115
|
+
placeholder: 'GameName#TAG',
|
|
116
|
+
hint: 'Your Riot ID including the tagline, e.g. GameName#TAG.',
|
|
117
|
+
pattern: /^[\p{L}\p{N}](?:[\p{L}\p{N} ]{1,14}[\p{L}\p{N}])?#[\p{L}\p{N}]{3,5}$/u,
|
|
118
|
+
min: 7,
|
|
119
|
+
max: 22,
|
|
120
|
+
},
|
|
121
|
+
epic: {
|
|
122
|
+
code: 'epic',
|
|
123
|
+
label: 'Epic display name',
|
|
124
|
+
placeholder: 'Epic display name',
|
|
125
|
+
hint: '3 to 16 letters, numbers, spaces or - _ . as shown on your Epic account.',
|
|
126
|
+
pattern: /^[\p{L}\p{N}][\p{L}\p{N} ._-]{1,14}[\p{L}\p{N}._-]$/u,
|
|
127
|
+
min: 3,
|
|
128
|
+
max: 16,
|
|
129
|
+
},
|
|
130
|
+
ea: {
|
|
131
|
+
code: 'ea',
|
|
132
|
+
label: 'EA ID',
|
|
133
|
+
placeholder: 'EA ID',
|
|
134
|
+
hint: '4 to 16 letters, numbers, hyphens or underscores.',
|
|
135
|
+
pattern: /^[A-Za-z0-9_-]{4,16}$/,
|
|
136
|
+
min: 4,
|
|
137
|
+
max: 16,
|
|
138
|
+
},
|
|
139
|
+
activision: {
|
|
140
|
+
code: 'activision',
|
|
141
|
+
label: 'Activision ID',
|
|
142
|
+
placeholder: 'Name#1234567',
|
|
143
|
+
hint: 'Your Activision ID including the number, e.g. Name#1234567.',
|
|
144
|
+
pattern: /^[A-Za-z0-9 _-]{2,16}#\d{4,8}$/,
|
|
145
|
+
min: 7,
|
|
146
|
+
max: 25,
|
|
147
|
+
},
|
|
148
|
+
ubisoft: {
|
|
149
|
+
code: 'ubisoft',
|
|
150
|
+
label: 'Ubisoft username',
|
|
151
|
+
placeholder: 'Ubisoft username',
|
|
152
|
+
hint: '3 to 15 letters, numbers or - _ . as shown in Ubisoft Connect.',
|
|
153
|
+
pattern: /^[A-Za-z0-9._-]{3,15}$/,
|
|
154
|
+
min: 3,
|
|
155
|
+
max: 15,
|
|
156
|
+
},
|
|
157
|
+
steam: {
|
|
158
|
+
code: 'steam',
|
|
159
|
+
label: 'Steam ID',
|
|
160
|
+
placeholder: 'Friend code or profile URL',
|
|
161
|
+
hint: 'Your Steam friend code (Friends > Add a Friend), your SteamID64, or your steamcommunity.com profile URL.',
|
|
162
|
+
// A profile URL is the easiest thing for a player to paste; what gets
|
|
163
|
+
// stored is the id or custom name at the end of it.
|
|
164
|
+
normalize: (value) => value.replace(STEAM_PROFILE_URL, '$1'),
|
|
165
|
+
pattern: /^[A-Za-z0-9_-]{3,32}$/,
|
|
166
|
+
min: 3,
|
|
167
|
+
max: 32,
|
|
168
|
+
},
|
|
169
|
+
nintendo: {
|
|
170
|
+
code: 'nintendo',
|
|
171
|
+
label: 'Nintendo friend code',
|
|
172
|
+
placeholder: 'SW-0000-0000-0000',
|
|
173
|
+
hint: 'Your 12-digit Switch friend code, e.g. SW-1234-5678-9012.',
|
|
174
|
+
normalize: (value) => {
|
|
175
|
+
const digits = value.replace(/^sw/i, '').replace(/[\s-]/g, '');
|
|
176
|
+
return /^\d{12}$/.test(digits)
|
|
177
|
+
? `SW-${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8)}`
|
|
178
|
+
: value;
|
|
179
|
+
},
|
|
180
|
+
pattern: /^SW-\d{4}-\d{4}-\d{4}$/,
|
|
181
|
+
min: 17,
|
|
182
|
+
max: 17,
|
|
183
|
+
},
|
|
184
|
+
// fightcade, apple and googleplay have no documented handle shape and stay
|
|
185
|
+
// on the generic rule.
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The rule a network's handles are checked against: its own when the
|
|
190
|
+
* catalogue row asks for format checks and a rule exists for its code,
|
|
191
|
+
* otherwise the generic one.
|
|
192
|
+
*/
|
|
193
|
+
export function handleRuleFor(
|
|
194
|
+
network:
|
|
195
|
+
| {
|
|
196
|
+
code?: GameAccountCode | string | null;
|
|
197
|
+
verification?: GameAccountVerification | string | null;
|
|
198
|
+
}
|
|
199
|
+
| null
|
|
200
|
+
| undefined
|
|
201
|
+
): HandleRule {
|
|
202
|
+
if (network?.verification !== 'format' || !network.code) {
|
|
203
|
+
return GENERIC_HANDLE_RULE;
|
|
204
|
+
}
|
|
205
|
+
return HANDLE_RULES[network.code as GameAccountCode] ?? GENERIC_HANDLE_RULE;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** True when the code has a rule of its own, so `format` means something. */
|
|
209
|
+
export function hasHandleRule(code: string | null | undefined): boolean {
|
|
210
|
+
return !!code && Object.prototype.hasOwnProperty.call(HANDLE_RULES, code);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Checks `raw` against `rule` and returns the value to store: trimmed, runs of
|
|
215
|
+
* whitespace collapsed to one space, then the rule's own normalization.
|
|
216
|
+
*
|
|
217
|
+
* The message is the one the player reads, so it says what a valid value looks
|
|
218
|
+
* like rather than which check failed.
|
|
219
|
+
*/
|
|
220
|
+
export function checkHandle(
|
|
221
|
+
raw: string | null | undefined,
|
|
222
|
+
rule: HandleRule
|
|
223
|
+
): HandleCheck {
|
|
224
|
+
const collapsed = (raw ?? '').trim().replace(/\s+/g, ' ');
|
|
225
|
+
if (!collapsed) {
|
|
226
|
+
return { ok: false, message: `Please enter your ${rule.label}` };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const value = rule.normalize ? rule.normalize(collapsed) : collapsed;
|
|
230
|
+
|
|
231
|
+
if (value.length < rule.min) {
|
|
232
|
+
return { ok: false, message: `${rule.label} is too short. ${rule.hint}` };
|
|
233
|
+
}
|
|
234
|
+
if (value.length > rule.max) {
|
|
235
|
+
return { ok: false, message: `${rule.label} is too long. ${rule.hint}` };
|
|
236
|
+
}
|
|
237
|
+
if (!rule.pattern.test(value)) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
message: `That doesn't look like a valid ${rule.label}. ${rule.hint}`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { ok: true, value };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The key two handles are compared on: same network, same letters, any case. */
|
|
248
|
+
export function normalizeHandleKey(value: string): string {
|
|
249
|
+
return value.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
250
|
+
}
|
package/src/manual/index.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Hand-written types that are not derived from a Strapi content type.
|
|
3
3
|
*/
|
|
4
4
|
export * from './country';
|
|
5
|
+
export * from './gameAccountHandle';
|
|
5
6
|
export * from './lobby';
|
|
6
7
|
export * from './notification';
|
|
8
|
+
export * from './playerBan';
|
|
7
9
|
export * from './tournamentEntry';
|
|
8
10
|
export * from './tournamentInvite';
|
|
9
11
|
export * from './tournamentJoinRequest';
|
|
@@ -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
|
};
|