3.4 Tenant Bans
Section 3 — Player Management
3.4 Tenant Bans
Scope
Covers how tenant admins and platform admins view and manage players within a tenant. Includes player listing, ban management, login enforcement, and platform-level player administration (block, reactivate, soft delete).
Two Levels of Management
| Level | Who | Route prefix | Scope |
|---|---|---|---|
| Tenant admin | SaaS user with admin or owner role on the tenant, or platform admin | /api/bus_tenants/\{tenantId\}/... and /api/admin/bus_tenants/\{tenantId\}/... | Players within one specific tenant |
| Platform admin | GamersLabAdmin or GamersLabOwner platform role | /api/admin/players | All players across the entire platform |
Tenant Admin — Player Listing
GET /api/admin/bus_tenants/{tenantId}/players
Authorization: Bearer <saas_user_jwt>Returns all players who have ever logged into the tenant. Includes ban status inline.
bannedAt, bannedUntil, and bannedReason are populated only when the ban is currently active.
Results are paginated.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | — | Optional prefix search on display name or email, or exact player ID match |
page | int | 1 | 1-based page number |
pageSize | int | 50 | Page size, max 200 |
includeInactive | bool | false | Include merged/inactive player profiles |
Required role: admin or owner on the tenant. Platform admins bypass the role check.
Returns (PagedTenantPlayerAdminResponseDto):
page int
pageSize int
totalCount int
items[]
playerId Guid
displayName string?
email string?
isActive bool
firstSeenAt DateTime
lastSeenAt DateTime
loginCount int
isBanned bool
bannedAt DateTime?
bannedUntil DateTime? — null if permanent active ban
bannedReason string?Tenant Admin — Ban Management
Bans are scoped to a single tenant. A player banned from one game is not affected in any other game.
Apply or Update a Ban
PUT /api/bus_tenants/{tenantId}/player-bans/{playerId}
Authorization: Bearer <saas_user_jwt>
Content-Type: application/jsonRequired role: admin or owner on the tenant. Platform admins bypass the role check.
Request body:
{
"bannedUntil": "2026-06-01T00:00:00Z",
"reason": "Cheating — third-party aimbots detected",
"metadata": {
"reportId": "RPT-12345",
"severity": "high"
}
}| Field | Required | Description |
|---|---|---|
bannedUntil | No | UTC expiry. Omit or set to null for a permanent ban. Must be in the future if provided, and must include Z or an explicit timezone offset. |
reason | No | Human-readable reason. Stored on the ban record; not exposed to the player. |
metadata | No | Arbitrary key-value pairs for internal tracking. |
This endpoint is an upsert — calling it on a player who is already banned updates the existing ban record.
If the supplied playerId is an old merged profile ID, the ban is applied to the current active
canonical profile.
The player must already have tenant access for this tenant. Admins cannot create bans for players
who have never logged into the game.
Returns (PlayerTenantBanResponse):
playerId Guid
tenantId Guid
isBanned bool
bannedAt DateTime?
bannedUntil DateTime?
reason string?
bannedByUserId Guid? — SaaS user ID who set the ban
metadata objectClear a Ban
DELETE /api/bus_tenants/{tenantId}/player-bans/{playerId}
Authorization: Bearer <saas_user_jwt>Required role: admin or owner on the tenant. Platform admins bypass the role check.
Returns 404 if no ban record exists for the player in this tenant.
Returns the final ban record with isBanned=false.
Clearing a ban is non-destructive: the original bannedAt, bannedUntil, reason, bannedByUserId,
and metadata remain on the ban record for audit history.
Ban Enforcement at Login
The ban check runs during every player login flow, regardless of provider. It is not possible to bypass enforcement by switching providers — the check is on the player profile ID, not the auth method.
Covered login endpoints:
POST /api/player-auth/login(Steam, Epic, Sequence, Mock)POST /api/player-auth/otc/exchange(EmailOneTimeCode)POST /api/player-auth/challenge/verifyPOST /api/player-auth/wallet/verify(EVM wallet)POST /api/player-auth/refresh
Active ban criteria: isBanned = true AND (bannedUntil is null OR bannedUntil > utcNow)
Expired bans are treated as inactive and do not block login. The ban record is not automatically deleted or updated when it expires — it remains with the past expiry date.
Response on blocked login: 403 Forbidden
{
"title": "Player Banned",
"detail": "Player is banned from this tenant until 2026-06-01T00:00:00Z. Reason: Cheating"
}The error message includes the expiry time (if set) and reason (if set). No internal IDs
or metadata are exposed to the player client. bannedByUserId is never exposed on player-facing
ban errors.
Ban Data Model
Table: game_player_tenant_bans
| Column | Type | Notes |
|---|---|---|
id | uuid | Row identifier |
player_id | uuid | FK → game_player_profiles.id |
tenant_id | uuid | FK → bus_tenants.id |
is_banned | bool | true = banned, false = cleared |
banned_at | timestamptz | When the ban was set |
banned_until | timestamptz? | Null = permanent |
banned_reason | text? | Internal reason string |
banned_by_user_id | uuid? | SaaS user who applied the ban |
ban_metadata | jsonb | Arbitrary metadata dict |
One row per (player_id, tenant_id) pair — bans are upserted, not appended.
Audit Trail
Both ban and unban actions are written to the tenant-scoped audit log automatically.
| Action | Event type |
|---|---|
| Ban applied / updated | player.tenant_ban.applied |
| Ban cleared | player.tenant_ban.cleared |
Each audit record captures: actorUserId, targetTenantId, playerId, isBanned,
bannedAt, bannedUntil, and reason.
Platform Admin — Global Player Management
Platform admins (GamersLabAdmin / GamersLabOwner) have a separate set of endpoints that
operate across the entire platform, not scoped to a single tenant.
List All Players
GET /api/admin/players?query=&limit=50&offset=0&includeInactive=false
Authorization: Bearer <platform_admin_jwt>Searches across all player profiles. query matches display name, email, or player ID.
Returns (PagedAdminPlayerResponseDto):
items[] AdminPlayerProfileDto[]
limit int
offset int
totalCount intitems[] shape:
id Guid
displayName string?
email string?
profileVisibility string
isActive bool
isEmailVerified bool
createdAt DateTime
updatedAt DateTime
mergedIntoId Guid?Get Single Player
GET /api/admin/players/{playerId}
Authorization: Bearer <platform_admin_jwt>Returns AdminPlayerDetailsDto, a platform-admin detail view. It extends
AdminPlayerProfileDto with:
avatarUrl string?
platformRole string
authMethods[] AuthMethodDTO[]
tenantAccess[] AdminPlayerTenantAccessDto[]
mergedProfileIds Guid[]Tenant access rows include current tenant-ban state: isBanned, bannedAt, bannedUntil,
and bannedReason.
Create Player (Admin-Provisioned)
POST /api/admin/players
Authorization: Bearer <platform_admin_jwt>
Content-Type: application/jsonCreates a player profile with email/password credentials directly, bypassing the normal signup flow. Returns 409 if the email is already registered.
Update Player
PUT /api/admin/players/{playerId}
Authorization: Bearer <platform_admin_jwt>
Content-Type: application/jsonUpdates displayName, avatarUrl, email, or profileVisibility on behalf of any player.
Block Player (Platform-Wide)
POST /api/admin/players/{playerId}/block
Authorization: Bearer <platform_admin_jwt>Sets isActive = false on the player profile. Blocked players cannot log in to any tenant.
This is a platform-level action — distinct from a tenant ban, which is game-specific.
Reactivate Player
POST /api/admin/players/{playerId}/reactivate
Authorization: Bearer <platform_admin_jwt>Sets isActive = true. Reverses a block. Does not affect any existing tenant bans.
Soft Delete (Redact) Player
DELETE /api/admin/players/{playerId}
Authorization: Bearer <platform_admin_jwt>Permanently redacts the player profile. PII is anonymized in-place — the record is not removed from the database, preserving foreign key integrity and audit history.
Ban vs Block — Key Distinction
| Tenant Ban | Platform Block | |
|---|---|---|
| Scope | One specific game/tenant | All tenants, platform-wide |
| Who sets it | Tenant admin, owner, or platform admin | Platform admin only |
| Effect | 403 at login for that tenant only | Cannot log in anywhere |
| Reversible | Yes — clear ban endpoint | Yes — reactivate endpoint |
| Expiry | Optional (bannedUntil) | No expiry; manual reactivation only |
| Audit logged | Yes | No dedicated audit event (uses profile update) |
| Reason stored | Yes | No |
Code References
- Ban controller:
GamersLabRestAPI/PlayerAuth/Controllers/PlayerTenantBansController.cs - Tenant player list controller:
GamersLabRestAPI/PlayerAuth/Controllers/TenantPlayersAdminController.cs - Platform admin controller:
GamersLabRestAPI/PlayerAuth/Controllers/PlayerAdminController.cs - Ban service:
GamersLabRestAPI/PlayerAuth/Services/PlayerTenantBanService.cs - Ban model:
GamersLabRestAPI/PlayerAuth/Models/PlayerTenantBan.cs - Ban DTOs:
GamersLabRestAPI/PlayerAuth/DTOs/PlayerTenantBanDTOs.cs - Admin player DTOs:
GamersLabRestAPI/PlayerAuth/DTOs/AdminPlayerDTOs.cs - Tenant player admin DTOs:
GamersLabRestAPI/PlayerAuth/DTOs/TenantPlayerAdminDTOs.cs - Banned exception:
GamersLabRestAPI/PlayerAuth/Exceptions/PlayerTenantBannedException.cs