From 9e6ec3d52abf36921dc60f3ee2286e1b6682282a Mon Sep 17 00:00:00 2001 From: Niek Candaele <22315101+niekcandaele@users.noreply.github.com> Date: Sat, 23 Dec 2023 13:25:33 +0100 Subject: [PATCH] feat: add steam info to players (#777) * feat: add steam info to players * fix: make steam api key optional * fix: some tests :) * more test-fixing --- .env.example | 2 + packages/app-api/src/config.ts | 26 ++++ .../src/controllers/PlayerController.ts | 10 +- packages/app-api/src/db/player.ts | 54 ++++++++ packages/app-api/src/lib/steamApi.ts | 121 ++++++++++++++++++ packages/app-api/src/service/PlayerService.ts | 69 +++++++++- .../__tests__/eventWorker.integration.test.ts | 27 ++-- .../app-api/src/workers/playerSyncWorker.ts | 29 +++-- packages/lib-apiclient/src/generated/api.ts | 96 ++++++++++++++ .../sql/20231222180438-steam-data.ts | 27 ++++ .../Expired roles get deleted.json | 30 +++-- ...r properly - expiring gameserver role.json | 56 ++++---- ...erver properly - expiring global role.json | 58 +++++---- packages/web-main/src/pages/Players.tsx | 69 ++++++++-- 14 files changed, 577 insertions(+), 97 deletions(-) create mode 100644 packages/app-api/src/lib/steamApi.ts create mode 100644 packages/lib-db/src/migrations/sql/20231222180438-steam-data.ts diff --git a/.env.example b/.env.example index 1f7b13701d..75d5aca837 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,8 @@ DISCORD_BOT_TOKEN= MAILHOG_URL="http://127.0.0.1:8025" +STEAM_API_KEY="" + ################################# # AWS ################################# diff --git a/packages/app-api/src/config.ts b/packages/app-api/src/config.ts index 6ce03f018e..ccbe55df9f 100644 --- a/packages/app-api/src/config.ts +++ b/packages/app-api/src/config.ts @@ -3,6 +3,7 @@ import { queuesConfigSchema, IQueuesConfig } from '@takaro/queues'; import { IDbConfig, configSchema as dbConfigSchema } from '@takaro/db'; import { IAuthConfig, authConfigSchema } from '@takaro/auth'; import { errors } from '@takaro/util'; +import ms from 'ms'; enum CLUSTER_MODE { SINGLE = 'single', @@ -33,6 +34,11 @@ interface IHttpConfig extends IBaseConfig { url: string; startWorkers: boolean; }; + steam: { + apiKey: string; + refreshOlderThanMs: number; + refreshBatchSize: number; + }; } const configSchema = { @@ -142,6 +148,26 @@ const configSchema = { env: 'START_WORKERS', }, }, + steam: { + apiKey: { + doc: 'The Steam API key', + format: String, + default: '', + env: 'STEAM_API_KEY', + }, + refreshOlderThanMs: { + doc: 'Players whose Steam data is older than this will be refreshed', + format: Number, + default: ms('1day'), + env: 'STEAM_REFRESH_OLDER_THAN_MS', + }, + refreshBatchSize: { + doc: 'The amount of players to refresh per time the sync runs', + format: Number, + default: 100, + env: 'STEAM_REFRESH_BATCH_SIZE', + }, + }, }; export const config = new Config>([ diff --git a/packages/app-api/src/controllers/PlayerController.ts b/packages/app-api/src/controllers/PlayerController.ts index 748cf803e6..1d12837758 100644 --- a/packages/app-api/src/controllers/PlayerController.ts +++ b/packages/app-api/src/controllers/PlayerController.ts @@ -1,4 +1,4 @@ -import { IsISO8601, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator'; +import { IsBoolean, IsISO8601, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator'; import { ITakaroQuery } from '@takaro/db'; import { APIOutput, apiResponse } from '@takaro/http'; import { PlayerOutputDTO, PlayerOutputWithRolesDTO, PlayerService } from '../service/PlayerService.js'; @@ -50,6 +50,14 @@ class PlayerSearchInputAllowedFilters { @IsOptional() @IsString({ each: true }) xboxLiveId!: string[]; + + @IsOptional() + @IsBoolean({ each: true }) + steamCommunityBanned!: boolean[]; + + @IsOptional() + @IsBoolean({ each: true }) + steamVacBanned!: boolean[]; } class PlayerSearchInputDTO extends ITakaroQuery { diff --git a/packages/app-api/src/db/player.ts b/packages/app-api/src/db/player.ts index 61d7415a94..6c2ed2e349 100644 --- a/packages/app-api/src/db/player.ts +++ b/packages/app-api/src/db/player.ts @@ -11,6 +11,18 @@ import { } from '../service/PlayerService.js'; import { ROLE_TABLE_NAME, RoleModel } from './role.js'; import { PLAYER_ON_GAMESERVER_TABLE_NAME, PlayerOnGameServerModel } from './playerOnGameserver.js'; +import { config } from '../config.js'; + +export interface ISteamData { + steamId: string; + steamAvatar: string; + steamAccountCreated: number; + steamCommunityBanned: boolean; + steamEconomyBan: string; + steamVacBanned: boolean; + steamsDaysSinceLastBan: number; + steamNumberOfVACBans: number; +} export const PLAYER_TABLE_NAME = 'players'; const ROLE_ON_PLAYER_TABLE_NAME = 'roleOnPlayer'; @@ -44,6 +56,15 @@ export class PlayerModel extends TakaroModel { xboxLiveId?: string; epicOnlineServicesId?: string; + steamLastFetch: Date; + steamAvatar: string; + steamAccountCreated: Date; + steamCommunityBanned: boolean; + steamEconomyBan: string; + steamVacBanned: boolean; + steamsDaysSinceLastBan: number; + steamNumberOfVACBans: number; + static get relationMappings() { return { gameServers: { @@ -184,4 +205,37 @@ export class PlayerRepo extends ITakaroRepo { + const { query } = await this.getModel(); + + const refreshOlderThanDate = new Date(Date.now() - config.get('steam.refreshOlderThanMs')).toISOString(); + + const players = await query + .select('steamId') + .where('steamId', 'is not', null) + .andWhere(function () { + this.where('steamLastFetch', '<', refreshOlderThanDate).orWhere('steamLastFetch', 'is', null); + }) + .orderBy('steamLastFetch', 'asc') + .limit(config.get('steam.refreshBatchSize')); + + return players.filter((item) => item.steamId).map((item) => item.steamId) as string[]; + } + + async setSteamData(data: (ISteamData | undefined)[]) { + const { query } = await this.getModel(); + await Promise.all( + data.map((item) => { + if (!item) return; + return query + .update({ + ...item, + steamAccountCreated: new Date(item.steamAccountCreated * 1000), + steamLastFetch: new Date(), + }) + .where('steamId', item.steamId); + }) + ); + } } diff --git a/packages/app-api/src/lib/steamApi.ts b/packages/app-api/src/lib/steamApi.ts new file mode 100644 index 0000000000..7907e3ae25 --- /dev/null +++ b/packages/app-api/src/lib/steamApi.ts @@ -0,0 +1,121 @@ +import { AxiosError, AxiosInstance } from 'axios'; +import axios from 'axios'; +import { config } from '../config.js'; +import { logger } from '@takaro/util'; + +interface IPlayerSummary { + steamid: string; + communityvisibilitystate: number; + profilestate: number; + personaname: string; + commentpermission: number; + profileurl: string; + avatar: string; + avatarmedium: string; + avatarfull: string; + avatarhash: string; + lastlogoff: number; + personastate: number; + primaryclanid: string; + timecreated: number; + personastateflags: number; +} + +interface IPlayerBans { + SteamId: string; + CommunityBanned: boolean; + VACBanned: boolean; + NumberOfVACBans: number; + DaysSinceLastBan: number; + NumberOfGameBans: number; + EconomyBan: string; +} + +class SteamApi { + private apiKey = config.get('steam.apiKey'); + private _client: AxiosInstance; + private log = logger('steamApi'); + + get client() { + if (!this._client) { + this._client = axios.create({ + baseURL: 'https://api.steampowered.com', + timeout: 1000, + }); + + this._client.interceptors.request.use((config) => { + config.params = config.params || {}; + config.params.key = this.apiKey; + return config; + }); + + axios.interceptors.request.use((request) => { + this.log.info(`➡️ ${request.method?.toUpperCase()} ${request.url}`, { + method: request.method, + url: request.url, + }); + return request; + }); + + axios.interceptors.response.use( + (response) => { + this.log.info( + `⬅️ ${response.request.method?.toUpperCase()} ${response.request.path} ${response.status} ${ + response.statusText + }`, + { + status: response.status, + statusText: response.statusText, + method: response.request.method, + url: response.request.url, + } + ); + + return response; + }, + (error: AxiosError) => { + let details = {}; + + if (error.response?.data) { + const data = error.response.data as Record; + details = JSON.stringify(data.meta); + } + + this.log.error('☠️ Request errored', { + traceId: error.response?.headers['x-trace-id'], + details, + status: error.response?.status, + statusText: error.response?.statusText, + method: error.config?.method, + url: error.config?.url, + response: error.response?.data, + }); + return Promise.reject(error); + } + ); + } + return this._client; + } + + async getPlayerSummaries(steamIds: string[]): Promise { + const response = await this.client.get('/ISteamUser/GetPlayerSummaries/v2', { + params: { + steamids: steamIds.join(','), + }, + }); + + return response.data.response.players; + } + + async getPlayerBans(steamIds: string[]): Promise { + const response = await this.client.get('/ISteamUser/GetPlayerBans/v1', { + params: { + steamids: steamIds.join(','), + }, + }); + + return response.data.players; + } +} + +export const steamApi = new SteamApi(); diff --git a/packages/app-api/src/service/PlayerService.ts b/packages/app-api/src/service/PlayerService.ts index b89e5f1895..fc87891657 100644 --- a/packages/app-api/src/service/PlayerService.ts +++ b/packages/app-api/src/service/PlayerService.ts @@ -1,7 +1,7 @@ import { TakaroService } from './Base.js'; -import { PlayerModel, PlayerRepo } from '../db/player.js'; -import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ISteamData, PlayerModel, PlayerRepo } from '../db/player.js'; +import { IsBoolean, IsISO8601, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; import { TakaroDTO, TakaroModelDTO, traceableClass } from '@takaro/util'; import { ITakaroQuery } from '@takaro/db'; import { PaginatedOutput } from '../db/base.js'; @@ -11,6 +11,8 @@ import { IPlayerReferenceDTO } from '@takaro/gameserver'; import { Type } from 'class-transformer'; import { PlayerRoleAssignmentOutputDTO, RoleService } from './RoleService.js'; import { EVENT_TYPES, EventCreateDTO, EventService } from './EventService.js'; +import { steamApi } from '../lib/steamApi.js'; +import { config } from '../config.js'; export class PlayerOutputDTO extends TakaroModelDTO { @IsString() @@ -26,6 +28,34 @@ export class PlayerOutputDTO extends TakaroModelDTO { @IsOptional() epicOnlineServicesId?: string; + @IsString() + @IsOptional() + steamAvatar?: string; + + @IsISO8601() + @IsOptional() + steamAccountCreated?: Date; + + @IsBoolean() + @IsOptional() + steamCommunityBanned?: boolean; + + @IsString() + @IsOptional() + steamEconomyBan?: string; + + @IsBoolean() + @IsOptional() + steamVacBanned?: boolean; + + @IsNumber() + @IsOptional() + steamsDaysSinceLastBan?: number; + + @IsNumber() + @IsOptional() + steamNumberOfVACBans?: number; + @IsOptional() @ValidateNested({ each: true }) @Type(() => PlayerOnGameserverOutputDTO) @@ -195,4 +225,39 @@ export class PlayerService extends TakaroService { + const summary = summaries.find((item) => item.steamid === steamId); + const ban = bans.find((item) => item.SteamId === steamId); + + if (!summary || !ban) { + this.log.warn('Steam data missing', { steamId, summary, ban }); + return; + } + + return { + steamId, + steamAvatar: summary.avatarfull, + steamAccountCreated: summary.timecreated, + steamCommunityBanned: ban.CommunityBanned, + steamEconomyBan: ban.EconomyBan, + steamVacBanned: ban.VACBanned, + steamsDaysSinceLastBan: ban.DaysSinceLastBan, + steamNumberOfVACBans: ban.NumberOfVACBans, + }; + }); + + await this.repo.setSteamData(fullData); + } } diff --git a/packages/app-api/src/workers/__tests__/eventWorker.integration.test.ts b/packages/app-api/src/workers/__tests__/eventWorker.integration.test.ts index 687f0af72d..f484a2c0c4 100644 --- a/packages/app-api/src/workers/__tests__/eventWorker.integration.test.ts +++ b/packages/app-api/src/workers/__tests__/eventWorker.integration.test.ts @@ -70,24 +70,27 @@ const tests = [ test: async function () { const playerService = new PlayerService(this.standardDomainId ?? ''); + const pogs = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({ + filters: { + gameId: ['1'], + gameServerId: [this.setupData[0].id], + }, + }); + + const pog = pogs.data.data[0]; + + if (!pog) throw new Error('No player on game server found'); + + const playerRes = await this.client.player.playerControllerGetOne(pog.playerId); + const MOCK_PLAYER = await new IGamePlayer().construct({ ip: '169.169.169.80', name: 'jefke', - gameId: '1', - steamId: '76561198021481871', + gameId: pog.gameId, + steamId: playerRes.data.data.steamId, }); await playerService.sync(MOCK_PLAYER, this.setupData[0].id); - - const playersRes = await this.client.player.playerControllerSearch({ - filters: { - steamId: [MOCK_PLAYER.steamId as string], - }, - extend: ['playerOnGameServers'], - }); - - expect(playersRes.data.data[0].playerOnGameServers).to.have.lengthOf(1); - await playerService.sync(MOCK_PLAYER, this.setupData[1].id); const playersResAfter = await this.client.player.playerControllerSearch({ diff --git a/packages/app-api/src/workers/playerSyncWorker.ts b/packages/app-api/src/workers/playerSyncWorker.ts index 83cde4ed47..a32efc3a0b 100644 --- a/packages/app-api/src/workers/playerSyncWorker.ts +++ b/packages/app-api/src/workers/playerSyncWorker.ts @@ -40,21 +40,26 @@ export async function processJob(job: Job) { const domains = await domainsService.find({}); for (const domain of domains.results) { - const gameserverService = new GameServerService(domain.id); - const gameServers = await gameserverService.find({}); + const promises = []; - const promises = gameServers.results.map(async (gs) => { - const reachable = await gameserverService.testReachability(gs.id); + const playerService = new PlayerService(domain.id); + promises.push(playerService.handleSteamSync()); - if (reachable.connectable) { - await queueService.queues.playerSync.queue.add( - { domainId: domain.id, gameServerId: gs.id }, - { jobId: `playerSync-${domain.id}-${gs.id}-${Date.now()}` } - ); - } - }); + const gameserverService = new GameServerService(domain.id); + const gameServers = await gameserverService.find({}); + promises.push( + gameServers.results.map(async (gs) => { + const reachable = await gameserverService.testReachability(gs.id); + if (reachable.connectable) { + await queueService.queues.playerSync.queue.add( + { domainId: domain.id, gameServerId: gs.id }, + { jobId: `playerSync-${domain.id}-${gs.id}-${Date.now()}` } + ); + } + }) + ); - await Promise.all(promises); + await Promise.allSettled(promises); } return; diff --git a/packages/lib-apiclient/src/generated/api.ts b/packages/lib-apiclient/src/generated/api.ts index 1e0476910c..485a6f70ae 100644 --- a/packages/lib-apiclient/src/generated/api.ts +++ b/packages/lib-apiclient/src/generated/api.ts @@ -4948,6 +4948,48 @@ export interface PlayerOutputDTO { * @memberof PlayerOutputDTO */ epicOnlineServicesId?: string; + /** + * + * @type {string} + * @memberof PlayerOutputDTO + */ + steamAvatar?: string; + /** + * + * @type {NOTDOMAINSCOPEDTakaroModelDTOCreatedAt} + * @memberof PlayerOutputDTO + */ + steamAccountCreated?: NOTDOMAINSCOPEDTakaroModelDTOCreatedAt; + /** + * + * @type {boolean} + * @memberof PlayerOutputDTO + */ + steamCommunityBanned?: boolean; + /** + * + * @type {string} + * @memberof PlayerOutputDTO + */ + steamEconomyBan?: string; + /** + * + * @type {boolean} + * @memberof PlayerOutputDTO + */ + steamVacBanned?: boolean; + /** + * + * @type {number} + * @memberof PlayerOutputDTO + */ + steamsDaysSinceLastBan?: number; + /** + * + * @type {number} + * @memberof PlayerOutputDTO + */ + steamNumberOfVACBans?: number; /** * * @type {Array} @@ -5046,6 +5088,48 @@ export interface PlayerOutputWithRolesDTO { * @memberof PlayerOutputWithRolesDTO */ epicOnlineServicesId?: string; + /** + * + * @type {string} + * @memberof PlayerOutputWithRolesDTO + */ + steamAvatar?: string; + /** + * + * @type {NOTDOMAINSCOPEDTakaroModelDTOCreatedAt} + * @memberof PlayerOutputWithRolesDTO + */ + steamAccountCreated?: NOTDOMAINSCOPEDTakaroModelDTOCreatedAt; + /** + * + * @type {boolean} + * @memberof PlayerOutputWithRolesDTO + */ + steamCommunityBanned?: boolean; + /** + * + * @type {string} + * @memberof PlayerOutputWithRolesDTO + */ + steamEconomyBan?: string; + /** + * + * @type {boolean} + * @memberof PlayerOutputWithRolesDTO + */ + steamVacBanned?: boolean; + /** + * + * @type {number} + * @memberof PlayerOutputWithRolesDTO + */ + steamsDaysSinceLastBan?: number; + /** + * + * @type {number} + * @memberof PlayerOutputWithRolesDTO + */ + steamNumberOfVACBans?: number; /** * * @type {Array} @@ -5182,6 +5266,18 @@ export interface PlayerSearchInputAllowedFilters { * @memberof PlayerSearchInputAllowedFilters */ xboxLiveId?: Array; + /** + * + * @type {Array} + * @memberof PlayerSearchInputAllowedFilters + */ + steamCommunityBanned?: Array; + /** + * + * @type {Array} + * @memberof PlayerSearchInputAllowedFilters + */ + steamVacBanned?: Array; } /** * diff --git a/packages/lib-db/src/migrations/sql/20231222180438-steam-data.ts b/packages/lib-db/src/migrations/sql/20231222180438-steam-data.ts new file mode 100644 index 0000000000..a332b58c6e --- /dev/null +++ b/packages/lib-db/src/migrations/sql/20231222180438-steam-data.ts @@ -0,0 +1,27 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('players', (table) => { + table.string('steamAvatar').nullable(); + table.timestamp('steamLastFetch').nullable(); + table.timestamp('steamAccountCreated').nullable(); + table.boolean('steamCommunityBanned').nullable(); + table.string('steamEconomyBan').nullable(); + table.boolean('steamVacBanned').nullable(); + table.string('steamsDaysSinceLastBan').nullable(); + table.string('steamNumberOfVACBans').nullable(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('players', (table) => { + table.dropColumn('steamAvatar'); + table.dropColumn('steamLastFetch'); + table.dropColumn('steamAccountCreated'); + table.dropColumn('steamCommunityBanned'); + table.dropColumn('steamEconomyBan'); + table.dropColumn('steamVacBanned'); + table.dropColumn('steamsDaysSinceLastBan'); + table.dropColumn('steamNumberOfVACBans'); + }); +} diff --git a/packages/test/src/__snapshots__/PlayerController/Expired roles get deleted.json b/packages/test/src/__snapshots__/PlayerController/Expired roles get deleted.json index af3063bc5a..ebe1010c7e 100644 --- a/packages/test/src/__snapshots__/PlayerController/Expired roles get deleted.json +++ b/packages/test/src/__snapshots__/PlayerController/Expired roles get deleted.json @@ -2,27 +2,35 @@ "body": { "meta": {}, "data": { - "id": "40e7888f-a8b3-45d7-997b-cf257f31dcc6", - "createdAt": "2023-11-19T15:52:07.026Z", - "updatedAt": "2023-11-19T15:52:07.026Z", - "name": "Melvina15", - "steamId": "q3albiue5x7zc6ch", + "id": "b91c2e8f-470d-48e0-93d8-6dac9945916b", + "createdAt": "2023-12-23T10:21:10.697Z", + "updatedAt": "2023-12-23T10:21:10.697Z", + "name": "Warren_Bahringer5", + "steamId": "fc8tk03el39wydpk", "xboxLiveId": null, "epicOnlineServicesId": null, + "steamAvatar": null, + "steamAccountCreated": null, + "steamCommunityBanned": null, + "steamEconomyBan": null, + "steamVacBanned": null, + "steamsDaysSinceLastBan": null, + "steamNumberOfVACBans": null, "roleAssignments": [ { - "playerId": "40e7888f-a8b3-45d7-997b-cf257f31dcc6", - "roleId": "bf674a66-4548-4a1b-8aad-12eba1f200ce", + "playerId": "b91c2e8f-470d-48e0-93d8-6dac9945916b", + "roleId": "2dd6691c-43bb-4eef-b7fc-45636439d2f0", "role": { - "id": "bf674a66-4548-4a1b-8aad-12eba1f200ce", - "createdAt": "2023-11-19T15:52:06.335Z", - "updatedAt": "2023-11-19T15:52:06.336Z", + "id": "2dd6691c-43bb-4eef-b7fc-45636439d2f0", + "createdAt": "2023-12-23T10:21:10.352Z", + "updatedAt": "2023-12-23T10:21:10.353Z", "name": "Player", "permissions": [], "system": true } } - ] + ], + "steamLastFetch": null } }, "status": 200, diff --git a/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring gameserver role.json b/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring gameserver role.json index 89e9751fc6..440ba4d243 100644 --- a/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring gameserver role.json +++ b/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring gameserver role.json @@ -2,53 +2,61 @@ "body": { "meta": {}, "data": { - "id": "1bc8b37c-0fdc-4a39-96af-a2b060d35927", - "createdAt": "2023-11-19T15:58:01.133Z", - "updatedAt": "2023-11-19T15:58:01.134Z", - "name": "Melvina15", - "steamId": "q3albiue5x7zc6ch", + "id": "2daf99d4-187c-40ab-a2a7-ec3aa59ad995", + "createdAt": "2023-12-23T10:21:12.572Z", + "updatedAt": "2023-12-23T10:21:12.572Z", + "name": "Henriette97", + "steamId": "r05vx9kwcza0oj4r", "xboxLiveId": null, "epicOnlineServicesId": null, + "steamAvatar": null, + "steamAccountCreated": null, + "steamCommunityBanned": null, + "steamEconomyBan": null, + "steamVacBanned": null, + "steamsDaysSinceLastBan": null, + "steamNumberOfVACBans": null, "roleAssignments": [ { - "id": "863c5e86-9804-45f4-97e8-f38de4081a05", - "createdAt": "2023-11-19T15:58:01.587Z", - "updatedAt": "2023-11-19T15:58:01.587Z", - "playerId": "1bc8b37c-0fdc-4a39-96af-a2b060d35927", - "roleId": "4fc5e760-a291-453e-9e1b-e6094dec112f", + "id": "29bc6583-855d-4101-843b-afd1dcbe8103", + "createdAt": "2023-12-23T10:21:14.157Z", + "updatedAt": "2023-12-23T10:21:14.157Z", + "playerId": "2daf99d4-187c-40ab-a2a7-ec3aa59ad995", + "roleId": "4002839a-a6eb-4397-b8be-726eeb4d4e17", "gameServerId": null, "expiresAt": null, "role": { - "id": "4fc5e760-a291-453e-9e1b-e6094dec112f", - "createdAt": "2023-11-19T15:58:01.508Z", - "updatedAt": "2023-11-19T15:58:01.508Z", + "id": "4002839a-a6eb-4397-b8be-726eeb4d4e17", + "createdAt": "2023-12-23T10:21:14.072Z", + "updatedAt": "2023-12-23T10:21:14.072Z", "name": "Global role", "system": false, "permissions": [ { - "id": "6ba1c41b-7fb9-47f0-9107-9402f271968c", - "createdAt": "2023-11-19T15:58:01.514Z", - "updatedAt": "2023-11-19T15:58:01.514Z", - "permissionId": "5ba0c0b8-5830-465e-aef5-1678897bbe38", - "roleId": "4fc5e760-a291-453e-9e1b-e6094dec112f", + "id": "dde8da1a-a554-43fe-b50e-400413f1163b", + "createdAt": "2023-12-23T10:21:14.077Z", + "updatedAt": "2023-12-23T10:21:14.078Z", + "permissionId": "dbba72fe-22bc-4683-af84-cbc1c352fbac", + "roleId": "4002839a-a6eb-4397-b8be-726eeb4d4e17", "count": 0 } ] } }, { - "playerId": "1bc8b37c-0fdc-4a39-96af-a2b060d35927", - "roleId": "b8fd3d89-d70f-49c6-bcae-d568d92834c2", + "playerId": "2daf99d4-187c-40ab-a2a7-ec3aa59ad995", + "roleId": "5d4d279b-d7e5-4973-9f3f-6efdcbd888f2", "role": { - "id": "b8fd3d89-d70f-49c6-bcae-d568d92834c2", - "createdAt": "2023-11-19T15:58:00.611Z", - "updatedAt": "2023-11-19T15:58:00.612Z", + "id": "5d4d279b-d7e5-4973-9f3f-6efdcbd888f2", + "createdAt": "2023-12-23T10:21:12.227Z", + "updatedAt": "2023-12-23T10:21:12.228Z", "name": "Player", "permissions": [], "system": true } } - ] + ], + "steamLastFetch": null } }, "status": 200, diff --git a/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring global role.json b/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring global role.json index 7dc80d6e8b..b9a77991ba 100644 --- a/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring global role.json +++ b/packages/test/src/__snapshots__/PlayerController/Handles expiring role of gameServer properly - expiring global role.json @@ -2,53 +2,61 @@ "body": { "meta": {}, "data": { - "id": "065c06e1-4483-40e3-8e2f-c4ad2b3b80fd", - "createdAt": "2023-11-19T15:58:02.286Z", - "updatedAt": "2023-11-19T15:58:02.287Z", - "name": "Melvina15", - "steamId": "q3albiue5x7zc6ch", + "id": "3939b6e4-b78b-43b5-b7e5-d8f51b81cb53", + "createdAt": "2023-12-23T10:21:14.688Z", + "updatedAt": "2023-12-23T10:21:14.688Z", + "name": "Jarred_Ledner", + "steamId": "9e1nw6ydnfz3qo04", "xboxLiveId": null, "epicOnlineServicesId": null, + "steamAvatar": null, + "steamAccountCreated": null, + "steamCommunityBanned": null, + "steamEconomyBan": null, + "steamVacBanned": null, + "steamsDaysSinceLastBan": null, + "steamNumberOfVACBans": null, "roleAssignments": [ { - "id": "390997db-ff2b-4418-934b-be19a57b9d21", - "createdAt": "2023-11-19T15:58:02.746Z", - "updatedAt": "2023-11-19T15:58:02.746Z", - "playerId": "065c06e1-4483-40e3-8e2f-c4ad2b3b80fd", - "roleId": "185426b9-b3cb-422e-8425-0ebc64c8a62f", - "gameServerId": "9a1ba1f7-65f9-40a8-a59a-89677d927728", + "id": "980732ab-1f33-4c5d-898e-200777e6435c", + "createdAt": "2023-12-23T10:21:16.283Z", + "updatedAt": "2023-12-23T10:21:16.283Z", + "playerId": "3939b6e4-b78b-43b5-b7e5-d8f51b81cb53", + "roleId": "e76c228c-5196-4259-976e-3882d57d6b00", + "gameServerId": "5fd76cb9-f54a-453c-a726-5f93d743b1a5", "expiresAt": null, "role": { - "id": "185426b9-b3cb-422e-8425-0ebc64c8a62f", - "createdAt": "2023-11-19T15:58:02.678Z", - "updatedAt": "2023-11-19T15:58:02.678Z", + "id": "e76c228c-5196-4259-976e-3882d57d6b00", + "createdAt": "2023-12-23T10:21:16.205Z", + "updatedAt": "2023-12-23T10:21:16.205Z", "name": "GameServer role", "system": false, "permissions": [ { - "id": "dafc0046-8cf3-46a3-9890-048ccd46172e", - "createdAt": "2023-11-19T15:58:02.683Z", - "updatedAt": "2023-11-19T15:58:02.683Z", - "permissionId": "5ba0c0b8-5830-465e-aef5-1678897bbe38", - "roleId": "185426b9-b3cb-422e-8425-0ebc64c8a62f", + "id": "7a125568-571b-4c21-8e9a-0468813edb9a", + "createdAt": "2023-12-23T10:21:16.209Z", + "updatedAt": "2023-12-23T10:21:16.210Z", + "permissionId": "dbba72fe-22bc-4683-af84-cbc1c352fbac", + "roleId": "e76c228c-5196-4259-976e-3882d57d6b00", "count": 0 } ] } }, { - "playerId": "065c06e1-4483-40e3-8e2f-c4ad2b3b80fd", - "roleId": "47017f8e-aee1-41ec-9a25-768fae01a0a0", + "playerId": "3939b6e4-b78b-43b5-b7e5-d8f51b81cb53", + "roleId": "99d159d5-bca9-4091-a334-6ecd828e60c3", "role": { - "id": "47017f8e-aee1-41ec-9a25-768fae01a0a0", - "createdAt": "2023-11-19T15:58:01.759Z", - "updatedAt": "2023-11-19T15:58:01.760Z", + "id": "99d159d5-bca9-4091-a334-6ecd828e60c3", + "createdAt": "2023-12-23T10:21:14.327Z", + "updatedAt": "2023-12-23T10:21:14.328Z", "name": "Player", "permissions": [], "system": true } } - ] + ], + "steamLastFetch": null } }, "status": 200, diff --git a/packages/web-main/src/pages/Players.tsx b/packages/web-main/src/pages/Players.tsx index 6b5cefa94d..3f9103e8d9 100644 --- a/packages/web-main/src/pages/Players.tsx +++ b/packages/web-main/src/pages/Players.tsx @@ -1,5 +1,5 @@ import { FC, Fragment } from 'react'; -import { Table, useTableActions, IconButton, Dropdown, PERMISSIONS } from '@takaro/lib-components'; +import { Table, useTableActions, IconButton, Dropdown, PERMISSIONS, styled } from '@takaro/lib-components'; import { PlayerOutputDTO, PlayerSearchInputDTOSortDirectionEnum } from '@takaro/apiclient'; import { createColumnHelper } from '@tanstack/react-table'; import { usePlayers } from 'queries/players'; @@ -9,6 +9,14 @@ import { AiOutlineUser as ProfileIcon, AiOutlineEdit as EditIcon, AiOutlineRight import { useDocumentTitle } from 'hooks/useDocumentTitle'; import { PermissionsGuard } from 'components/PermissionsGuard'; +const SteamAvatar = styled.img` + width: 2rem; + height: 2rem; + border-radius: 50%; + margin: auto; + display: block; +`; + const Players: FC = () => { useDocumentTitle('Players'); const { pagination, columnFilters, sorting, columnSearch } = useTableActions(); @@ -40,10 +48,24 @@ const Players: FC = () => { // IMPORTANT: id should be identical to data object key. const columnHelper = createColumnHelper(); const columnDefs = [ + columnHelper.accessor('steamAvatar', { + header: '', + id: 'steamAvatar', + cell: (info) => { + const avatar = info.getValue(); + if (!avatar) return ; + return ; + }, + enableColumnFilter: true, + }), columnHelper.accessor('name', { header: 'Name', id: 'name', - cell: (info) => info.getValue(), + cell: (info) => { + const name = info.getValue(); + if (!name) return ''; + return {name}; + }, enableColumnFilter: true, enableSorting: true, }), @@ -68,17 +90,44 @@ const Players: FC = () => { enableColumnFilter: true, enableSorting: true, }), - columnHelper.accessor('createdAt', { - header: 'Created at', - id: 'createdAt', - meta: { type: 'datetime' }, + + columnHelper.accessor('steamAccountCreated', { + header: 'Account Created', + id: 'steamAccountCreated', + cell: (info) => { + const date = info.getValue(); + if (!date) return ''; + return new Date(date).toLocaleDateString(); + }, + enableSorting: true, + }), + columnHelper.accessor('steamCommunityBanned', { + header: 'Community Banned', + id: 'steamCommunityBanned', + cell: (info) => (info.getValue() ? 'Yes' : 'No'), + enableColumnFilter: true, + }), + columnHelper.accessor('steamEconomyBan', { + header: 'Economy Ban', + id: 'steamEconomyBan', + cell: (info) => info.getValue(), + enableColumnFilter: true, + }), + columnHelper.accessor('steamVacBanned', { + header: 'VAC Banned', + id: 'steamVacBanned', + cell: (info) => (info.getValue() ? 'Yes' : 'No'), + enableColumnFilter: true, + }), + columnHelper.accessor('steamsDaysSinceLastBan', { + header: 'Days Since Last Ban', + id: 'steamsDaysSinceLastBan', cell: (info) => info.getValue(), enableSorting: true, }), - columnHelper.accessor('updatedAt', { - header: 'Updated at', - id: 'updatedAt', - meta: { type: 'datetime' }, + columnHelper.accessor('steamNumberOfVACBans', { + header: 'Number of VAC Bans', + id: 'steamNumberOfVACBans', cell: (info) => info.getValue(), enableSorting: true, }),