From acc967e50a0953b784cf0835d3a701457affb75c Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sun, 5 Jun 2022 16:27:56 -0500 Subject: [PATCH] add ban management page --- SharedLibraryCore/Utilities.cs | 2 + WebfrontCore/Controllers/ActionController.cs | 11 +- WebfrontCore/Controllers/AdminController.cs | 29 ++- .../BanInfoResourceQueryHelper.cs | 234 ++++++++++++++---- WebfrontCore/QueryHelpers/Models/BanInfo.cs | 31 ++- .../QueryHelpers/Models/BanInfoRequest.cs | 3 + WebfrontCore/ViewModels/TableInfo.cs | 30 ++- WebfrontCore/Views/Admin/BanManagement.cshtml | 66 +++++ WebfrontCore/Views/Admin/_BanEntries.cshtml | 63 +++++ WebfrontCore/Views/Shared/_DataTable.cshtml | 52 +++- WebfrontCore/Views/Shared/_LeftNavBar.cshtml | 49 ++-- 11 files changed, 484 insertions(+), 86 deletions(-) create mode 100644 WebfrontCore/Views/Admin/BanManagement.cshtml create mode 100644 WebfrontCore/Views/Admin/_BanEntries.cshtml diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index b95e9565e..7be105c55 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -47,6 +47,8 @@ namespace SharedLibraryCore public static char[] DirectorySeparatorChars = { '\\', '/' }; public static char CommandPrefix { get; set; } = '!'; + public static string ToStandardFormat(this DateTime? time) => time?.ToString("yyyy-MM-dd H:mm:ss"); + public static EFClient IW4MAdminClient(Server server = null) { return new EFClient diff --git a/WebfrontCore/Controllers/ActionController.cs b/WebfrontCore/Controllers/ActionController.cs index 6fb7711ff..352211670 100644 --- a/WebfrontCore/Controllers/ActionController.cs +++ b/WebfrontCore/Controllers/ActionController.cs @@ -142,7 +142,7 @@ namespace WebfrontCore.Controllers })); } - public IActionResult UnbanForm() + public IActionResult UnbanForm(long? id) { var info = new ActionInfo { @@ -159,6 +159,15 @@ namespace WebfrontCore.Controllers Action = "UnbanAsync", ShouldRefresh = true }; + if (id is not null) + { + info.Inputs.Add(new() + { + Name = "targetId", + Value = id.ToString(), + Type = "hidden" + }); + } return View("_ActionForm", info); } diff --git a/WebfrontCore/Controllers/AdminController.cs b/WebfrontCore/Controllers/AdminController.cs index a809ea907..ab1687ad1 100644 --- a/WebfrontCore/Controllers/AdminController.cs +++ b/WebfrontCore/Controllers/AdminController.cs @@ -4,6 +4,7 @@ using SharedLibraryCore; using SharedLibraryCore.Dtos; using SharedLibraryCore.Interfaces; using System.Threading.Tasks; +using WebfrontCore.QueryHelpers.Models; namespace WebfrontCore.Controllers { @@ -11,12 +12,16 @@ namespace WebfrontCore.Controllers { private readonly IAuditInformationRepository _auditInformationRepository; private readonly ITranslationLookup _translationLookup; + private readonly IResourceQueryHelper _banInfoQueryHelper; private static readonly int DEFAULT_COUNT = 25; - public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository, ITranslationLookup translationLookup) : base(manager) + public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository, + ITranslationLookup translationLookup, + IResourceQueryHelper banInfoQueryHelper) : base(manager) { _auditInformationRepository = auditInformationRepository; _translationLookup = translationLookup; + _banInfoQueryHelper = banInfoQueryHelper; } [Authorize] @@ -27,7 +32,7 @@ namespace WebfrontCore.Controllers ViewBag.Title = _translationLookup["WEBFRONT_NAV_AUDIT_LOG"]; ViewBag.InitialOffset = DEFAULT_COUNT; - var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest() + var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest { Count = DEFAULT_COUNT }); @@ -41,5 +46,25 @@ namespace WebfrontCore.Controllers var auditItems = await _auditInformationRepository.ListAuditInformation(paginationInfo); return PartialView("_ListAuditLog", auditItems); } + + public async Task BanManagement([FromQuery] BanInfoRequest request) + { + var results = await _banInfoQueryHelper.QueryResource(request); + + ViewBag.ClientName = request.ClientName; + ViewBag.ClientId = request.ClientId; + ViewBag.ClientIP = request.ClientIP; + ViewBag.ClientGuid = request.ClientGuid; + + ViewBag.Title = "Ban Management"; + + return View(results.Results); + } + + public async Task BanManagementList([FromQuery] BanInfoRequest request) + { + var results = await _banInfoQueryHelper.QueryResource(request); + return PartialView("_BanEntries", results.Results); + } } } diff --git a/WebfrontCore/QueryHelpers/BanInfoResourceQueryHelper.cs b/WebfrontCore/QueryHelpers/BanInfoResourceQueryHelper.cs index 69ceeab08..aa0a95872 100644 --- a/WebfrontCore/QueryHelpers/BanInfoResourceQueryHelper.cs +++ b/WebfrontCore/QueryHelpers/BanInfoResourceQueryHelper.cs @@ -1,6 +1,10 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading.Tasks; using Data.Abstractions; +using Data.Models; using Data.Models.Client; using Microsoft.EntityFrameworkCore; using SharedLibraryCore; @@ -21,16 +25,26 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper> QueryResource(BanInfoRequest query) { - if (query.Count > 30) + if (query.Count > 10) { - query.Count = 30; + query.Count = 10; } - - await using var context = _contextFactory.CreateContext(false); - var matchingClients = await context.Clients.Where(client => - EF.Functions.ILike(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name, $"%{query.ClientName.Trim()}%")) - .Where(client => client.Level == EFClient.Permission.Banned) + await using var context = _contextFactory.CreateContext(false); + + var iqMatchingClients = context.Clients.Where(client => client.Level == EFClient.Permission.Banned); + iqMatchingClients = SetupSearchArgs(query, iqMatchingClients); + + if (string.IsNullOrEmpty(query.ClientName) && string.IsNullOrEmpty(query.ClientGuid) && + query.ClientId is null && string.IsNullOrEmpty(query.ClientIP)) + { + return new ResourceQueryHelperResult + { + Results = Enumerable.Empty() + }; + } + + var matchingClients = await iqMatchingClients .OrderByDescending(client => client.LastConnection) .Skip(query.Offset) .Take(query.Count) @@ -39,52 +53,117 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper matchingClients.Select(client => client.AliasLinkId).Contains(alias.LinkId)) - .Where(alias => alias.IPAddress != null) - .Select(alias => new { alias.IPAddress, alias.LinkId }) - .ToListAsync(); + var results = new List(); + var matchedClientIds = new List(); + var lateDateTime = DateTime.Now.AddYears(100); - var usedIpsGrouped = usedIps - .GroupBy(alias => alias.LinkId) - .ToDictionary(key => key.Key, value => value.Select(alias => alias.IPAddress).Distinct()); - - var searchingNetworkIds = matchingClients.Select(client => client.NetworkId); - var searchingIps = usedIpsGrouped.SelectMany(group => group.Value); - - var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier => - searchingNetworkIds.Contains(identifier.NetworkId) || - searchingIps.Contains(identifier.IPv4Address)) - .Select(penalty => new - { - penalty.CreatedDateTime, - PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name, - Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense) ? penalty.Penalty.Offense : "Anticheat Detection", - LinkId = penalty.Penalty.Offender.AliasLinkId, - penalty.Penalty.PunisherId - }) - .ToListAsync(); - - var groupedPenalties = matchedPenalties.GroupBy(penalty => penalty.LinkId) - .ToDictionary(key => key.Key, value => value.FirstOrDefault()); - - var results = matchingClients.Select(client => + // would prefer not to loop this, but unfortunately due to the data design + // we can't properly group on ip and alias link + foreach (var matchingClient in matchingClients) { - var matchedPenalty = - groupedPenalties.ContainsKey(client.AliasLinkId) ? groupedPenalties[client.AliasLinkId] : null; - return new BanInfo + var usedIps = await context.Aliases + .Where(alias => matchingClient.AliasLinkId == alias.LinkId) + .Where(alias => alias.IPAddress != null) + .Select(alias => new { alias.IPAddress, alias.LinkId }) + .ToListAsync(); + + var searchingNetworkId = matchingClient.NetworkId; + var searchingIps = usedIps.Select(ip => ip.IPAddress).Distinct(); + + var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier => + identifier.NetworkId == searchingNetworkId || + searchingIps.Contains(identifier.IPv4Address)) + .Where(identifier => identifier.Penalty.Expires == null || identifier.Penalty.Expires > lateDateTime) + .Select(penalty => new + { + penalty.CreatedDateTime, + PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name, + OffenderName = penalty.Penalty.Offender.CurrentAlias.Name, + Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense) + ? penalty.Penalty.Offense + : "Anticheat Detection", + LinkId = penalty.Penalty.Offender.AliasLinkId, + penalty.Penalty.OffenderId, + penalty.Penalty.PunisherId, + penalty.IPv4Address, + penalty.Penalty.Offender.NetworkId + }) + .ToListAsync(); + + if (!matchedPenalties.Any()) { - DateTime = matchedPenalty?.CreatedDateTime, - OffenderName = client.Name.StripColors(), - OffenderId = client.ClientId, - PunisherName = matchedPenalty?.PunisherName.StripColors(), - PunisherId = matchedPenalty?.PunisherId, - Offense = matchedPenalty?.Offense - }; - }).ToList(); + var linkIds = (await context.Aliases + .Where(alias => alias.IPAddress != null && searchingIps.Contains(alias.IPAddress)) + .Select(alias => alias.LinkId) + .ToListAsync()).Distinct(); + + + matchedPenalties = await context.Penalties.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Ban) + .Where(penalty => penalty.Expires == null || penalty.Expires > lateDateTime) + .Where(penalty => penalty.LinkId != null && linkIds.Contains(penalty.LinkId.Value)) + .OrderByDescending(penalty => penalty.When) + .Select(penalty => new + { + CreatedDateTime = penalty.When, + PunisherName = penalty.Punisher.CurrentAlias.Name, + OffenderName = penalty.Offender.CurrentAlias.Name, + Offense = string.IsNullOrEmpty(penalty.AutomatedOffense) + ? penalty.Offense + : "Anticheat Detection", + LinkId = penalty.Offender.AliasLinkId, + penalty.OffenderId, + penalty.PunisherId, + IPv4Address = penalty.Offender.CurrentAlias.IPAddress, + penalty.Offender.NetworkId + }).ToListAsync(); + } + + var allPenalties = matchedPenalties.Select(penalty => new PenaltyInfo + { + DateTime = penalty.CreatedDateTime, + Offense = penalty.Offense, + PunisherInfo = new RelatedClientInfo + { + ClientName = penalty.PunisherName.StripColors(), + ClientId = penalty.PunisherId, + }, + OffenderInfo = new RelatedClientInfo + { + ClientName = penalty.OffenderName.StripColors(), + ClientId = penalty.OffenderId, + IPAddress = penalty.IPv4Address, + NetworkId = penalty.NetworkId + } + }).ToList(); + + + if (matchedClientIds.Contains(matchingClient.ClientId)) + { + continue; + } + + matchedClientIds.Add(matchingClient.ClientId); + var relatedEntities = + allPenalties.Where(penalty => penalty.OffenderInfo.ClientId != matchingClient.ClientId); + + matchedClientIds.AddRange(relatedEntities.Select(client => client.OffenderInfo.ClientId)); + + results.Add(new BanInfo + { + ClientName = matchingClient.Name.StripColors(), + ClientId = matchingClient.ClientId, + NetworkId = matchingClient.NetworkId, + IPAddress = matchingClient.IPAddress, + + AssociatedPenalties = relatedEntities, + AttachedPenalty = allPenalties.FirstOrDefault(penalty => + penalty.OffenderInfo.ClientId == matchingClient.ClientId) + }); + } return new ResourceQueryHelperResult { @@ -93,4 +172,61 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper SetupSearchArgs(BanInfoRequest query, IQueryable source) + { + if (!string.IsNullOrEmpty(query.ClientName)) + { + var nameToSearch = query.ClientName.Trim().ToLower(); + source = source.Where(client => + EF.Functions.Like(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name.ToLower(), + $"%{nameToSearch}%")); + } + + if (!string.IsNullOrEmpty(query.ClientGuid)) + { + long? parsedGuid = null; + if (!long.TryParse(query.ClientGuid, NumberStyles.HexNumber, null, out var guid)) + { + if (!long.TryParse(query.ClientGuid, out var guid2)) + { + } + else + { + parsedGuid = guid2; + } + } + else + { + parsedGuid = guid; + } + + if (parsedGuid is not null) + { + source = source.Where(client => client.NetworkId == parsedGuid); + } + } + + if (query.ClientId is not null) + { + source = source.Where(client => client.ClientId == query.ClientId); + } + + if (string.IsNullOrEmpty(query.ClientIP)) + { + return source; + } + + var parsedIp = query.ClientIP.ConvertToIP(); + if (parsedIp is not null) + { + source = source.Where(client => client.CurrentAlias.IPAddress == parsedIp); + } + else + { + query.ClientIP = null; + } + + return source; + } } diff --git a/WebfrontCore/QueryHelpers/Models/BanInfo.cs b/WebfrontCore/QueryHelpers/Models/BanInfo.cs index 8fdd1adc2..a9e060c8c 100644 --- a/WebfrontCore/QueryHelpers/Models/BanInfo.cs +++ b/WebfrontCore/QueryHelpers/Models/BanInfo.cs @@ -1,12 +1,33 @@ using System; +using System.Collections.Generic; + +namespace WebfrontCore.QueryHelpers.Models; public class BanInfo { - public string OffenderName { get; set; } - public int OffenderId { get; set; } - public string PunisherName { get; set; } - public int? PunisherId { get; set; } + public string ClientName { get; set; } + public int ClientId { get; set; } + public int? IPAddress { get; set; } + public long NetworkId { get; set; } + public PenaltyInfo AttachedPenalty { get; set; } + public IEnumerable AssociatedPenalties { get; set; } +} + +public class PenaltyInfo +{ + public RelatedClientInfo OffenderInfo { get; set; } + public RelatedClientInfo PunisherInfo { get; set; } public string Offense { get; set; } public DateTime? DateTime { get; set; } - public long? TimeStamp => DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null; + + public long? TimeStamp => + DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null; +} + +public class RelatedClientInfo +{ + public string ClientName { get; set; } + public int? ClientId { get; set; } + public int? IPAddress { get; set; } + public long? NetworkId { get; set; } } diff --git a/WebfrontCore/QueryHelpers/Models/BanInfoRequest.cs b/WebfrontCore/QueryHelpers/Models/BanInfoRequest.cs index c92363050..4e3adbd68 100644 --- a/WebfrontCore/QueryHelpers/Models/BanInfoRequest.cs +++ b/WebfrontCore/QueryHelpers/Models/BanInfoRequest.cs @@ -5,4 +5,7 @@ namespace WebfrontCore.QueryHelpers.Models; public class BanInfoRequest : PaginationRequest { public string ClientName { get; set; } + public string ClientGuid { get; set; } + public int? ClientId { get; set; } + public string ClientIP { get; set; } } diff --git a/WebfrontCore/ViewModels/TableInfo.cs b/WebfrontCore/ViewModels/TableInfo.cs index 86fa48da1..b0b7888f5 100644 --- a/WebfrontCore/ViewModels/TableInfo.cs +++ b/WebfrontCore/ViewModels/TableInfo.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Html; namespace WebfrontCore.ViewModels; @@ -19,7 +20,7 @@ public class TableInfo public class RowDefinition { - public List Datum { get; } = new(); + public List Datum { get; } = new(); } public class ColumnDefinition @@ -28,6 +29,23 @@ public class ColumnDefinition public string ColumnSpan { get; set; } } +public enum ColumnType +{ + Text, + Link, + Icon, + Button +} + +public class ColumnTypeDefinition +{ + public ColumnType Type { get; set; } + public string Value { get; set; } + public string Data { get; set; } + public IHtmlContent Template { get; set; } + public int Id { get; set; } +} + public static class TableInfoExtensions { public static TableInfo WithColumns(this TableInfo info, IEnumerable columns) @@ -42,6 +60,16 @@ public static class TableInfoExtensions public static TableInfo WithRows(this TableInfo info, IEnumerable source, Func> selector) + { + return WithRows(info, source, (outer) => selector(outer).Select(item => new ColumnTypeDefinition + { + Value = item, + Type = ColumnType.Text + })); + } + + public static TableInfo WithRows(this TableInfo info, IEnumerable source, + Func> selector) { info.Rows.AddRange(source.Select(row => { diff --git a/WebfrontCore/Views/Admin/BanManagement.cshtml b/WebfrontCore/Views/Admin/BanManagement.cshtml new file mode 100644 index 000000000..0b017f50e --- /dev/null +++ b/WebfrontCore/Views/Admin/BanManagement.cshtml @@ -0,0 +1,66 @@ +@model IEnumerable + +
+

@ViewBag.Title

+ @if (!Model.Any()) + { +
Search for records...
+ } +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + @if (Model.Any()) + { +
+ +
+ } +
+ +@section scripts { + +} diff --git a/WebfrontCore/Views/Admin/_BanEntries.cshtml b/WebfrontCore/Views/Admin/_BanEntries.cshtml new file mode 100644 index 000000000..5bd2180f9 --- /dev/null +++ b/WebfrontCore/Views/Admin/_BanEntries.cshtml @@ -0,0 +1,63 @@ +@model IEnumerable + +@foreach (var ban in Model) +{ + if (ban.AttachedPenalty is null && !ban.AssociatedPenalties.Any()) + { + continue; + } + +
+
+
+
+ @ban.ClientName + +
@ban.NetworkId.ToString("X")
+
+ +
@ban.IPAddress.ConvertIPtoString()
+
+ @if (ban.AttachedPenalty is not null) + { +
+
@ban.AttachedPenalty.Offense.CapClientName(30)
+
@ban.AttachedPenalty.DateTime.ToStandardFormat()
+
Unban
+ } + else + { +
+
+ + Link-Only Ban +
+ } +
+ +
+ + @foreach (var associatedEntity in ban.AssociatedPenalties) + { +
+ + +
@associatedEntity.OffenderInfo.NetworkId?.ToString("X")
+
+ +
@associatedEntity.OffenderInfo.IPAddress.ConvertIPtoString()
+
+
+
@associatedEntity.Offense.CapClientName(30)
+
@associatedEntity.DateTime.ToStandardFormat()
+
Unban
+
+ } +
+
+} diff --git a/WebfrontCore/Views/Shared/_DataTable.cshtml b/WebfrontCore/Views/Shared/_DataTable.cshtml index 4139a730e..bf3a88439 100644 --- a/WebfrontCore/Views/Shared/_DataTable.cshtml +++ b/WebfrontCore/Views/Shared/_DataTable.cshtml @@ -1,4 +1,5 @@ -@model WebfrontCore.ViewModels.TableInfo +@using WebfrontCore.ViewModels +@model WebfrontCore.ViewModels.TableInfo @{ Layout = null; } @@ -17,7 +18,7 @@ - @{ var start = 0;} + @{ var start = 0; } @if (!Model.Rows.Any()) { @@ -35,15 +36,40 @@ @foreach (var row in Model.Rows) { - + @for (var i = 0; i < Model.Columns.Count; i++) { - @row.Datum[i] + var data = row.Datum[i]; + + @if (data.Template is null) + { + if (data.Type == ColumnType.Text) + { + @data.Value + } + if (data.Type == ColumnType.Link) + { + @data.Value + } + if (data.Type == ColumnType.Icon) + { + + } + if (data.Type == ColumnType.Button) + { +
@data.Value
+ } + } + else + { + @data.Template + } + } - + @foreach (var column in Model.Columns) { @@ -53,7 +79,21 @@ @for (var i = 0; i < Model.Columns.Count; i++) { -
@row.Datum[i]
+ var data = row.Datum[i]; +
+ @if (data.Type == ColumnType.Text) + { + @data.Value + } + @if (data.Type == ColumnType.Link) + { + @data.Value + } + @if (data.Type == ColumnType.Icon) + { + + } +
} diff --git a/WebfrontCore/Views/Shared/_LeftNavBar.cshtml b/WebfrontCore/Views/Shared/_LeftNavBar.cshtml index 475b305a9..22506ed78 100644 --- a/WebfrontCore/Views/Shared/_LeftNavBar.cshtml +++ b/WebfrontCore/Views/Shared/_LeftNavBar.cshtml @@ -115,30 +115,35 @@ @ViewBag.Localization["WEBFRONT_NAV_CONSOLE"] - @if (ViewBag.User.Level >= EFClient.Permission.Owner) - { - - - Configuration - - } - - - - @ViewBag.Localization["WEBFRONT_NAV_AUDIT_LOG"] - - - - - - @ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"] - - - - - @ViewBag.Localization["WEBFRONT_ACTION_TOKEN"] + + + + Ban Management + @if (ViewBag.User.Level >= EFClient.Permission.Owner) + { + + + Configuration + + } + + + + @ViewBag.Localization["WEBFRONT_NAV_AUDIT_LOG"] + + + + + + @ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"] + + + + + @ViewBag.Localization["WEBFRONT_ACTION_TOKEN"] + @if (ViewBag.Authorized) {