Skip to content

Commit 86c5411

Browse files
feat: Refactor player intelligence data handling and improve AJAX response structure
1 parent f571b98 commit 86c5411

8 files changed

Lines changed: 65 additions & 292 deletions

File tree

src/XtremeIdiots.Portal.Web.Tests/ApiControllers/DataControllerTests.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,6 @@ public void Constructor_WithNullConfiguration_ThrowsArgumentNullException()
8181
null!));
8282
}
8383

84-
[Fact]
85-
public async Task GetPlayersAjax_WithEmptyBody_ReturnsBadRequest()
86-
{
87-
// Arrange - empty body causes deserialization to return null model,
88-
// which triggers BadRequest from the null model guard
89-
var sut = CreateSut();
90-
sut.HttpContext.Request.Body = new MemoryStream();
91-
92-
// Act
93-
var result = await sut.GetPlayersAjax(null, null);
94-
95-
// Assert
96-
Assert.IsType<BadRequestResult>(result);
97-
}
98-
9984
[Fact]
10085
public async Task GetMapListAjax_WithEmptyBody_ReturnsBadRequest()
10186
{

src/XtremeIdiots.Portal.Web/ApiControllers/DataController.cs

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,6 @@ public class DataController(
3636
IConfiguration configuration) : BaseApiController(telemetryClient, logger, configuration)
3737
{
3838

39-
/// <summary>
40-
/// Retrieves players data for DataTables AJAX requests with username and GUID filtering
41-
/// </summary>
42-
/// <param name="id">Optional game type to filter players by</param>
43-
/// <param name="cancellationToken">Cancellation token for the async operation</param>
44-
/// <returns>DataTables-compatible JSON response with player data</returns>
45-
[HttpPost("Players/GetPlayersAjax")]
46-
[Authorize(Policy = AuthPolicies.AccessPlayers)]
47-
public async Task<IActionResult> GetPlayersAjax(GameType? id, [FromQuery] PlayersFilter? playersFilter, CancellationToken cancellationToken = default)
48-
{
49-
var filter = playersFilter ?? PlayersFilter.UsernameAndGuid;
50-
return await GetPlayersAjaxPrivate(filter, id, cancellationToken).ConfigureAwait(false);
51-
}
52-
5339
/// <summary>
5440
/// Retrieves maps data for DataTables AJAX requests with sorting and filtering support
5541
/// </summary>
@@ -176,69 +162,4 @@ public async Task<IActionResult> GetUsersAjax(CancellationToken cancellationToke
176162
}, nameof(GetUsersAjax)).ConfigureAwait(false);
177163
}
178164

179-
/// <summary>
180-
/// Private helper method to handle different types of player AJAX requests with filtering and sorting
181-
/// </summary>
182-
/// <param name="filter">The filter type to apply to player queries</param>
183-
/// <param name="gameType">Optional game type to filter players by</param>
184-
/// <param name="cancellationToken">Cancellation token for the async operation</param>
185-
/// <returns>DataTables-compatible JSON response with filtered player data</returns>
186-
private async Task<IActionResult> GetPlayersAjaxPrivate(PlayersFilter filter, GameType? gameType, CancellationToken cancellationToken = default)
187-
{
188-
return await ExecuteWithErrorHandlingAsync(async () =>
189-
{
190-
var reader = new StreamReader(Request.Body);
191-
var requestBody = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
192-
193-
var model = JsonConvert.DeserializeObject<DataTableAjaxPostModel>(requestBody);
194-
195-
if (model is null)
196-
{
197-
Logger.LogWarning("Invalid request model for players AJAX from user {UserId}", User.XtremeIdiotsId());
198-
return BadRequest();
199-
}
200-
201-
var order = PlayersOrder.LastSeenDesc;
202-
203-
if (model.Order?.Count > 0)
204-
{
205-
var orderColumn = model.Columns[model.Order.First().Column].Name;
206-
var searchOrder = model.Order.First().Dir;
207-
208-
order = orderColumn switch
209-
{
210-
"username" => searchOrder == "asc" ? PlayersOrder.UsernameAsc : PlayersOrder.UsernameDesc,
211-
"gameType" => searchOrder == "asc" ? PlayersOrder.GameTypeAsc : PlayersOrder.GameTypeDesc,
212-
"firstSeen" => searchOrder == "asc" ? PlayersOrder.FirstSeenAsc : PlayersOrder.FirstSeenDesc,
213-
"lastSeen" => searchOrder == "asc" ? PlayersOrder.LastSeenAsc : PlayersOrder.LastSeenDesc,
214-
_ => order
215-
};
216-
}
217-
218-
var playersApiResponse = await repositoryApiClient.Players.V1.GetPlayers(
219-
gameType, filter, model.Search?.Value, model.Start, model.Length, order, PlayerEntityOptions.None).ConfigureAwait(false);
220-
221-
if (!playersApiResponse.IsSuccess || playersApiResponse.Result?.Data is null)
222-
{
223-
Logger.LogError("Failed to retrieve players for user {UserId} with filter {Filter}",
224-
User.XtremeIdiotsId(), filter);
225-
return StatusCode(500, "Failed to retrieve players data");
226-
}
227-
228-
TrackSuccessTelemetry("PlayersListLoaded", nameof(GetPlayersAjax), new Dictionary<string, string>
229-
{
230-
{ "Filter", filter.ToString() },
231-
{ "GameType", gameType?.ToString() ?? "All" },
232-
{ "ResultCount", playersApiResponse.Result.Data.Items?.Count().ToString() ?? "0" }
233-
});
234-
235-
return Ok(new
236-
{
237-
model.Draw,
238-
recordsTotal = playersApiResponse.Result?.Pagination?.TotalCount,
239-
recordsFiltered = playersApiResponse.Result?.Pagination?.FilteredCount,
240-
data = playersApiResponse?.Result?.Data?.Items
241-
});
242-
}, nameof(GetPlayersAjax), $"filter: {filter}, gameType: {gameType}").ConfigureAwait(false);
243-
}
244165
}

src/XtremeIdiots.Portal.Web/ApiControllers/PlayersController.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,27 @@ public async Task<IActionResult> GetPlayersAjax(GameType? id, [FromQuery] Player
6565
return StatusCode(500, "Failed to retrieve players data");
6666
}
6767

68-
var enrichedPlayers = await playerCollectionApiResponse.Result.Data.Items
69-
.EnrichWithIntelligenceDataAsync(geoLocationClient, Logger, cancellationToken).ConfigureAwait(false);
68+
var intelligenceData = await playerCollectionApiResponse.Result.Data.Items
69+
.GetIntelligenceDataAsync(geoLocationClient, Logger, cancellationToken).ConfigureAwait(false);
7070

71-
var playerData = enrichedPlayers.Select(player => new
71+
var playerData = playerCollectionApiResponse.Result.Data.Items.Select(player =>
7272
{
73-
player.PlayerId,
74-
player.GameType,
75-
player.Username,
76-
player.Guid,
77-
player.IpAddress,
78-
player.FirstSeen,
79-
player.LastSeen,
80-
ProxyCheckRiskScore = player.ProxyCheckRiskScore(),
81-
IsProxy = player.IsProxy(),
82-
IsVpn = player.IsVpn(),
83-
ProxyType = player.ProxyType(),
84-
CountryCode = player.CountryCode()
73+
var intel = intelligenceData.TryGetValue(player.PlayerId, out var d) ? d : new PlayerIntelligenceData();
74+
return new
75+
{
76+
player.PlayerId,
77+
player.GameType,
78+
player.Username,
79+
player.Guid,
80+
player.IpAddress,
81+
player.FirstSeen,
82+
player.LastSeen,
83+
intel.ProxyCheckRiskScore,
84+
intel.IsProxy,
85+
intel.IsVpn,
86+
intel.ProxyType,
87+
intel.CountryCode
88+
};
8589
}).ToList();
8690

8791
TrackSuccessTelemetry("PlayersDataLoaded", nameof(GetPlayersAjax), new Dictionary<string, string>

src/XtremeIdiots.Portal.Web/Extensions/IPAddressExtensions.cs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using Microsoft.AspNetCore.Html;
22
using System.Text;
3-
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.Players;
4-
using XtremeIdiots.Portal.Web.Models;
53

64
namespace XtremeIdiots.Portal.Web.Extensions;
75

@@ -65,23 +63,6 @@ public static HtmlString FormatIPAddress(
6563
return new HtmlString(sb.ToString());
6664
}
6765

68-
public static HtmlString FormatIPAddress(
69-
this PlayerDto player,
70-
string? countryCode = null,
71-
bool linkToDetails = true)
72-
{
73-
return player is null || string.IsNullOrEmpty(player.IpAddress)
74-
? HtmlString.Empty
75-
: FormatIPAddress(
76-
player.IpAddress,
77-
countryCode ?? player.CountryCode(),
78-
player.ProxyCheckRiskScore(),
79-
player.IsProxy(),
80-
player.IsVpn(),
81-
player.ProxyType(),
82-
linkToDetails);
83-
}
84-
8566
public static string GetRiskClass(int riskScore)
8667
{
8768
return riskScore switch

src/XtremeIdiots.Portal.Web/Extensions/PlayerEnrichmentExtensions.cs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,62 +7,66 @@ namespace XtremeIdiots.Portal.Web.Extensions;
77

88
public static class PlayerEnrichmentExtensions
99
{
10-
public async static Task<PlayerDto> EnrichWithIntelligenceDataAsync(
10+
public async static Task<PlayerIntelligenceData> GetIntelligenceDataAsync(
1111
this PlayerDto playerDto,
1212
IGeoLocationApiClient geoLocationClient,
1313
ILogger logger,
1414
CancellationToken cancellationToken = default)
1515
{
16-
if (playerDto is null)
17-
return playerDto!;
18-
19-
if (string.IsNullOrEmpty(playerDto.IpAddress))
20-
return playerDto;
16+
if (playerDto is null || string.IsNullOrEmpty(playerDto.IpAddress))
17+
return new PlayerIntelligenceData();
2118

2219
try
2320
{
2421
var result = await geoLocationClient.GeoLookup.V1_1.GetIpIntelligence(playerDto.IpAddress, cancellationToken).ConfigureAwait(false);
2522
if (result.IsSuccess && result.Result?.Data is not null)
2623
{
2724
var intelligence = result.Result.Data;
28-
29-
if (!string.IsNullOrWhiteSpace(intelligence.CountryCode))
30-
{
31-
playerDto.SetCountryCode(intelligence.CountryCode);
32-
}
25+
var countryCode = !string.IsNullOrWhiteSpace(intelligence.CountryCode) ? intelligence.CountryCode : string.Empty;
3326

3427
if (intelligence.ProxyCheckStatus == SourceStatus.Success && intelligence.ProxyCheck is not null)
3528
{
36-
playerDto.SetProxyCheckRiskScore(intelligence.ProxyCheck.RiskScore);
37-
playerDto.SetIsProxy(intelligence.ProxyCheck.IsProxy);
38-
playerDto.SetIsVpn(intelligence.ProxyCheck.IsVpn);
39-
playerDto.SetProxyType(intelligence.ProxyCheck.ProxyType);
29+
return new PlayerIntelligenceData
30+
{
31+
CountryCode = countryCode,
32+
ProxyCheckRiskScore = intelligence.ProxyCheck.RiskScore,
33+
IsProxy = intelligence.ProxyCheck.IsProxy,
34+
IsVpn = intelligence.ProxyCheck.IsVpn,
35+
ProxyType = intelligence.ProxyCheck.ProxyType
36+
};
4037
}
38+
39+
return new PlayerIntelligenceData { CountryCode = countryCode };
4140
}
4241
}
4342
catch (Exception ex)
4443
{
4544
logger.LogWarning(ex, "Failed to enrich player DTO with intelligence data for IP {IpAddress}", playerDto.IpAddress);
4645
}
4746

48-
return playerDto;
47+
return new PlayerIntelligenceData();
4948
}
5049

51-
public async static Task<IEnumerable<PlayerDto>> EnrichWithIntelligenceDataAsync(
50+
public async static Task<Dictionary<Guid, PlayerIntelligenceData>> GetIntelligenceDataAsync(
5251
this IEnumerable<PlayerDto> playerDtos,
5352
IGeoLocationApiClient geoLocationClient,
5453
ILogger logger,
5554
CancellationToken cancellationToken = default)
5655
{
56+
var result = new Dictionary<Guid, PlayerIntelligenceData>();
57+
5758
if (playerDtos is null)
58-
return [];
59+
return result;
5960

60-
var players = playerDtos.ToList();
61+
var players = playerDtos.Where(p => p != null).ToList();
62+
var bag = new System.Collections.Concurrent.ConcurrentDictionary<Guid, PlayerIntelligenceData>();
6163

62-
await Parallel.ForEachAsync(players.Where(p => p != null), cancellationToken, async (player, ct) =>
63-
await player.EnrichWithIntelligenceDataAsync(geoLocationClient, logger, ct).ConfigureAwait(false))
64-
.ConfigureAwait(false);
64+
await Parallel.ForEachAsync(players, cancellationToken, async (player, ct) =>
65+
{
66+
var data = await player.GetIntelligenceDataAsync(geoLocationClient, logger, ct).ConfigureAwait(false);
67+
bag[player.PlayerId] = data;
68+
}).ConfigureAwait(false);
6569

66-
return players;
70+
return new Dictionary<Guid, PlayerIntelligenceData>(bag);
6771
}
6872
}

0 commit comments

Comments
 (0)