diff --git a/Application/Application.csproj b/Application/Application.csproj index 58966aa01..9a2f117d3 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -25,6 +25,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -63,6 +64,9 @@ Always + + Always + diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 5e86ca13c..ac1dfbd24 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -508,7 +508,8 @@ namespace IW4MAdmin { Origin = E.Origin, Target = E.Target, - Reason = E.Data + Reason = E.Data, + ReportedOn = DateTime.UtcNow }); var newReport = new EFPenalty() @@ -645,7 +646,7 @@ namespace IW4MAdmin } } - ChatHistory.Add(new ChatInfo() + ChatHistory.Add(new ChatInfo { Name = E.Origin.Name, Message = message, diff --git a/Application/Main.cs b/Application/Main.cs index 506297c64..b3f3107e2 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -447,6 +447,7 @@ namespace IW4MAdmin.Application .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) .AddTransient() .AddSingleton(translationLookup) .AddDatabaseContextOptions(appConfig); diff --git a/Application/Meta/MetaRegistration.cs b/Application/Meta/MetaRegistration.cs index 7d4cd8270..7f6771aea 100644 --- a/Application/Meta/MetaRegistration.cs +++ b/Application/Meta/MetaRegistration.cs @@ -83,7 +83,6 @@ namespace IW4MAdmin.Application.Meta Value = lastMapMeta.Value, ShouldDisplay = true, Type = MetaType.Information, - Column = 1, Order = 6 }); } @@ -101,8 +100,7 @@ namespace IW4MAdmin.Application.Meta Value = lastServerMeta.Value, ShouldDisplay = true, Type = MetaType.Information, - Column = 0, - Order = 6 + Order = 7 }); } @@ -120,8 +118,7 @@ namespace IW4MAdmin.Application.Meta Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"], Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(), ShouldDisplay = true, - Column = 1, - Order = 0, + Order = 8, Type = MetaType.Information }); @@ -131,8 +128,7 @@ namespace IW4MAdmin.Application.Meta Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"], Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(), ShouldDisplay = true, - Column = 1, - Order = 1, + Order = 9, Type = MetaType.Information }); @@ -142,8 +138,7 @@ namespace IW4MAdmin.Application.Meta Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"], Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(), ShouldDisplay = true, - Column = 1, - Order = 2, + Order = 10, Type = MetaType.Information }); @@ -154,8 +149,7 @@ namespace IW4MAdmin.Application.Meta Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), ShouldDisplay = true, - Column = 1, - Order = 3, + Order = 11, Type = MetaType.Information }); @@ -167,8 +161,7 @@ namespace IW4MAdmin.Application.Meta ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"], IsSensitive = true, - Column = 1, - Order = 4, + Order = 12, Type = MetaType.Information }); diff --git a/Application/Misc/GeoLocationResult.cs b/Application/Misc/GeoLocationResult.cs new file mode 100644 index 000000000..3f557550c --- /dev/null +++ b/Application/Misc/GeoLocationResult.cs @@ -0,0 +1,13 @@ +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Application.Misc; + +public class GeoLocationResult : IGeoLocationResult +{ + public string Country { get; set; } + public string CountryCode { get; set; } + public string Region { get; set; } + public string ASN { get; set; } + public string Timezone { get; set; } + public string Organization { get; set; } +} diff --git a/Application/Misc/GeoLocationService.cs b/Application/Misc/GeoLocationService.cs new file mode 100644 index 000000000..62f586745 --- /dev/null +++ b/Application/Misc/GeoLocationService.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using MaxMind.GeoIP2; +using MaxMind.GeoIP2.Responses; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Application.Misc; + +public class GeoLocationService : IGeoLocationService +{ + private readonly string _sourceAddress; + + public GeoLocationService(string sourceAddress) + { + _sourceAddress = sourceAddress; + } + + public Task Locate(string address) + { + CountryResponse country = null; + + try + { + using var reader = new DatabaseReader(_sourceAddress); + reader.TryCountry(address, out country); + } + catch + { + // ignored + } + + var response = new GeoLocationResult + { + Country = country?.Country.Name ?? "Unknown", + CountryCode = country?.Country.IsoCode ?? "" + }; + + return Task.FromResult((IGeoLocationResult)response); + } +} diff --git a/Application/Misc/MetaServiceV2.cs b/Application/Misc/MetaServiceV2.cs index eff8bd665..6f925db69 100644 --- a/Application/Misc/MetaServiceV2.cs +++ b/Application/Misc/MetaServiceV2.cs @@ -446,54 +446,7 @@ public class MetaServiceV2 : IMetaServiceV2 private static IEnumerable ProcessInformationMeta(IEnumerable meta) where T : IClientMeta { - var metaList = meta.ToList(); - var metaWithColumn = metaList - .Where(m => m.Column != null) - .ToList(); - - var columnGrouping = metaWithColumn - .GroupBy(m => m.Column) - .ToList(); - - var metaToSort = metaList.Except(metaWithColumn).ToList(); - - var table = columnGrouping.Select(metaItem => new List(metaItem)).ToList(); - - while (metaToSort.Count > 0) - { - var sortingMeta = metaToSort.First(); - - int IndexOfSmallestColumn() - { - var index = 0; - var smallestColumnSize = int.MaxValue; - for (var i = 0; i < table.Count; i++) - { - if (table[i].Count >= smallestColumnSize) - { - continue; - } - - smallestColumnSize = table[i].Count; - index = i; - } - - return index; - } - - var columnIndex = IndexOfSmallestColumn(); - - sortingMeta.Column = columnIndex; - sortingMeta.Order = columnGrouping - .First(group => group.Key == columnIndex) - .Count(); - - table[columnIndex].Add(sortingMeta); - - metaToSort.Remove(sortingMeta); - } - - return metaList; + return meta; } private static bool ValidArgs(string key, int clientId) => !string.IsNullOrWhiteSpace(key) && clientId > 0; diff --git a/Application/Resources/GeoLite2-Country.mmdb b/Application/Resources/GeoLite2-Country.mmdb new file mode 100644 index 000000000..6948a5244 Binary files /dev/null and b/Application/Resources/GeoLite2-Country.mmdb differ diff --git a/Plugins/LiveRadar/Controllers/RadarController.cs b/Plugins/LiveRadar/Controllers/RadarController.cs index 56a620375..f32a17224 100644 --- a/Plugins/LiveRadar/Controllers/RadarController.cs +++ b/Plugins/LiveRadar/Controllers/RadarController.cs @@ -24,20 +24,22 @@ namespace LiveRadar.Web.Controllers [HttpGet] [Route("Radar/{serverId}")] - public IActionResult Index(long? serverId = null) + public IActionResult Index(string serverId = null) { - ViewBag.IsFluid = true; - ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"]; - ViewBag.ActiveServerId = serverId ?? _manager.GetServers().FirstOrDefault()?.EndPoint; - ViewBag.Servers = _manager.GetServers() - .Where(_server => _server.GameName == Server.Game.IW4) - .Select(_server => new ServerInfo() + var servers = _manager.GetServers() + .Where(server => server.GameName == Server.Game.IW4) + .Select(server => new ServerInfo { - Name = _server.Hostname, - ID = _server.EndPoint + Name = server.Hostname, + IPAddress = server.IP, + Port = server.Port }); + + ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"]; + ViewBag.SelectedServerId = string.IsNullOrEmpty(serverId) ? servers.FirstOrDefault()?.Endpoint : serverId; - return View(); + // ReSharper disable once Mvc.ViewNotResolved + return View("~/Views/Plugins/LiveRadar/Radar/Index.cshtml", servers); } [HttpGet] diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index aee4a1873..e70b7ed0c 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -15,13 +15,6 @@ - - - - Never - - - diff --git a/Plugins/LiveRadar/Views/Radar/Index.cshtml b/Plugins/LiveRadar/Views/Radar/Index.cshtml deleted file mode 100644 index 9874860ba..000000000 --- a/Plugins/LiveRadar/Views/Radar/Index.cshtml +++ /dev/null @@ -1,53 +0,0 @@ -@model IEnumerable - - -
-
- -
-
- -
-
-
-
-
-
- -
-
- -
-
-
- - - - -@section scripts { - - - - - -} diff --git a/Plugins/LiveRadar/Views/_ViewImports.cshtml b/Plugins/LiveRadar/Views/_ViewImports.cshtml deleted file mode 100644 index 1ba5217d2..000000000 --- a/Plugins/LiveRadar/Views/_ViewImports.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@using SharedLibraryCore -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, SharedLibraryCore \ No newline at end of file diff --git a/Plugins/Stats/Extensions.cs b/Plugins/Stats/Extensions.cs index 22acfdc9a..67ee16b1c 100644 --- a/Plugins/Stats/Extensions.cs +++ b/Plugins/Stats/Extensions.cs @@ -193,4 +193,4 @@ namespace IW4MAdmin.Plugins.Stats throw new ArgumentException("No filters specified for chat search"); } } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/BaseController.cs b/SharedLibraryCore/BaseController.cs index 628fe11ab..6bc33a4ab 100644 --- a/SharedLibraryCore/BaseController.cs +++ b/SharedLibraryCore/BaseController.cs @@ -130,7 +130,7 @@ namespace SharedLibraryCore new Claim(ClaimTypes.NameIdentifier, Client.CurrentAlias.Name), new Claim(ClaimTypes.Role, Client.Level.ToString()), new Claim(ClaimTypes.Sid, Client.ClientId.ToString()), - new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")) + new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")), }; var claimsIdentity = new ClaimsIdentity(claims, "login"); SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait(); @@ -161,6 +161,14 @@ namespace SharedLibraryCore ViewBag.EnablePrivilegedUserPrivacy = AppConfig.EnablePrivilegedUserPrivacy; ViewBag.Configuration = AppConfig; ViewBag.ScriptInjection = AppConfig.Webfront?.ScriptInjection; + ViewBag.CommunityInformation = AppConfig.CommunityInformation; + ViewBag.ClientCount = Manager.GetServers().Sum(server => server.ClientNum); + ViewBag.AdminCount = Manager.GetServers().Sum(server => + server.GetClientsAsList() + .Count(client => client.Level >= Data.Models.Client.EFClient.Permission.Trusted)); + ViewBag.ReportCount = Manager.GetServers().Sum(server => + server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24))); + ViewBag.PermissionsSet = PermissionsSet; base.OnActionExecuting(context); } diff --git a/SharedLibraryCore/Dtos/ClientInfo.cs b/SharedLibraryCore/Dtos/ClientInfo.cs index 5cf4dcbf8..5e05fcd0d 100644 --- a/SharedLibraryCore/Dtos/ClientInfo.cs +++ b/SharedLibraryCore/Dtos/ClientInfo.cs @@ -1,12 +1,14 @@ -using Data.Models.Client; +using System; +using Data.Models.Client; namespace SharedLibraryCore.Dtos { - public class ClientInfo + public class ClientInfo { public string Name { get; set; } public int ClientId { get; set; } public int LinkId { get; set; } public EFClient.Permission Level { get; set; } + public DateTime LastConnection { get; set; } } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/Dtos/PlayerInfo.cs b/SharedLibraryCore/Dtos/PlayerInfo.cs index d44516b55..7bc424d69 100644 --- a/SharedLibraryCore/Dtos/PlayerInfo.cs +++ b/SharedLibraryCore/Dtos/PlayerInfo.cs @@ -15,8 +15,8 @@ namespace SharedLibraryCore.Dtos public int LevelInt { get; set; } public string IPAddress { get; set; } public long NetworkId { get; set; } - public List Aliases { get; set; } - public List IPs { get; set; } + public List<(string, DateTime)> Aliases { get; set; } + public List<(string, DateTime)> IPs { get; set; } public bool HasActivePenalty { get; set; } public string ActivePenaltyType { get; set; } public bool Authenticated { get; set; } @@ -29,5 +29,8 @@ namespace SharedLibraryCore.Dtos public IDictionary LinkedAccounts { get; set; } public MetaType? MetaFilterType { get; set; } public double? ZScore { get; set; } + public string ConnectProtocolUrl { get;set; } + public string CurrentServerName { get; set; } + public IGeoLocationResult GeoLocationInfo { get; set; } } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/Dtos/ServerInfo.cs b/SharedLibraryCore/Dtos/ServerInfo.cs index f06993ac9..1c7397406 100644 --- a/SharedLibraryCore/Dtos/ServerInfo.cs +++ b/SharedLibraryCore/Dtos/ServerInfo.cs @@ -20,6 +20,7 @@ namespace SharedLibraryCore.Dtos public bool Online { get; set; } public string ConnectProtocolUrl { get; set; } public string IPAddress { get; set; } + public string ExternalIPAddress { get; set; } public bool IsPasswordProtected { get; set; } public string Endpoint => $"{IPAddress}:{Port}"; diff --git a/SharedLibraryCore/Helpers/Report.cs b/SharedLibraryCore/Helpers/Report.cs index 9a1b96aae..79271ec4d 100644 --- a/SharedLibraryCore/Helpers/Report.cs +++ b/SharedLibraryCore/Helpers/Report.cs @@ -1,4 +1,5 @@ -using SharedLibraryCore.Database.Models; +using System; +using SharedLibraryCore.Database.Models; namespace SharedLibraryCore.Helpers { @@ -7,5 +8,6 @@ namespace SharedLibraryCore.Helpers public EFClient Target { get; set; } public EFClient Origin { get; set; } public string Reason { get; set; } + public DateTime ReportedOn { get; set; } } -} \ No newline at end of file +} diff --git a/SharedLibraryCore/Interfaces/IGeoLocationResult.cs b/SharedLibraryCore/Interfaces/IGeoLocationResult.cs new file mode 100644 index 000000000..c137347f4 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IGeoLocationResult.cs @@ -0,0 +1,11 @@ +namespace SharedLibraryCore.Interfaces; + +public interface IGeoLocationResult +{ + string Country { get; set; } + string CountryCode { get; set; } + string Region { get; set; } + string ASN { get; set; } + string Timezone { get; set; } + string Organization { get; set; } +} diff --git a/SharedLibraryCore/Interfaces/IGeoLocationService.cs b/SharedLibraryCore/Interfaces/IGeoLocationService.cs new file mode 100644 index 000000000..cfa592f45 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IGeoLocationService.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace SharedLibraryCore.Interfaces; + +public interface IGeoLocationService +{ + Task Locate(string address); +} diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 3a74fd264..5b4efb27f 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -117,7 +117,7 @@ namespace SharedLibraryCore public int ClientNum { - get { return Clients.ToArray().Count(p => p != null && !p.IsBot); } + get { return Clients.ToArray().Count(p => p != null && Utilities.IsDevelopment || (!p?.IsBot ?? false)); } } public int MaxClients { get; protected set; } diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 2acb4f346..fdfb906c3 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -529,32 +529,36 @@ namespace SharedLibraryCore public static bool HasPermission(this IEnumerable permissionsSet, TEntity entity, TPermission permission) where TEntity : Enum where TPermission : Enum { - return permissionsSet?.Any(raw => + if (permissionsSet == null) { - if (raw == "*") + return false; + } + + var requiredPermission = $"{entity.ToString()}.{permission.ToString()}"; + var hasAllPermissions = permissionsSet.Any(p => p.Equals("*")); + var permissionCheckResult = permissionsSet.Select(p => + { + if (p.Equals(requiredPermission, StringComparison.InvariantCultureIgnoreCase)) { return true; } - - var split = raw.Split("."); - if (split.Length != 2) + if (p.Equals($"-{requiredPermission}", StringComparison.InvariantCultureIgnoreCase)) { return false; } - if (!Enum.TryParse(typeof(TEntity), split[0], out var e)) - { - return false; - } + return (bool?)null; + }).ToList(); - if (!Enum.TryParse(typeof(TPermission), split[1], out var p)) - { - return false; - } + var permissionNegated = permissionCheckResult.Any(result => result.HasValue && !result.Value); - return (e?.Equals(entity) ?? false) && (p?.Equals(permission) ?? false); - }) ?? false; + if (permissionNegated) + { + return false; + } + + return hasAllPermissions || permissionCheckResult.Any(result => result.HasValue && result.Value); } public static bool HasPermission(this ApplicationConfiguration appConfig, diff --git a/WebfrontCore/Controllers/API/ClientController.cs b/WebfrontCore/Controllers/API/ClientController.cs index 792b5477c..810e2c1ba 100644 --- a/WebfrontCore/Controllers/API/ClientController.cs +++ b/WebfrontCore/Controllers/API/ClientController.cs @@ -80,7 +80,7 @@ namespace WebfrontCore.Controllers.API [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task LoginAsync([FromRoute] int clientId, + public async Task Login([FromRoute] int clientId, [FromBody, Required] PasswordRequest request) { if (clientId == 0) @@ -145,7 +145,7 @@ namespace WebfrontCore.Controllers.API [HttpPost("{clientId:int}/logout")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task LogoutAsync() + public async Task Logout() { if (Authorized) { @@ -170,4 +170,4 @@ namespace WebfrontCore.Controllers.API public string Password { get; set; } } } -} \ No newline at end of file +} diff --git a/WebfrontCore/Controllers/AccountController.cs b/WebfrontCore/Controllers/AccountController.cs index 91d283339..523ccdf43 100644 --- a/WebfrontCore/Controllers/AccountController.cs +++ b/WebfrontCore/Controllers/AccountController.cs @@ -19,11 +19,11 @@ namespace WebfrontCore.Controllers } [HttpGet] - public async Task LoginAsync(int clientId, string password) + public async Task Login(int clientId, string password) { if (clientId == 0 || string.IsNullOrEmpty(password)) { - return Unauthorized(); + return Unauthorized("Invalid credentials"); } try @@ -63,20 +63,20 @@ namespace WebfrontCore.Controllers : HttpContext.Connection.RemoteIpAddress.ToString() }); - return Ok(); + return Ok($"Welcome {privilegedClient.Name}. You are now logged in"); } } catch (Exception) { - return Unauthorized(); + return Unauthorized("Could not validate credentials"); } - return Unauthorized(); + return Unauthorized("Invalid credentials"); } [HttpGet] - public async Task LogoutAsync() + public async Task Logout() { if (Authorized) { diff --git a/WebfrontCore/Controllers/ActionController.cs b/WebfrontCore/Controllers/ActionController.cs index c0d7241b6..1a79e2b5b 100644 --- a/WebfrontCore/Controllers/ActionController.cs +++ b/WebfrontCore/Controllers/ActionController.cs @@ -69,25 +69,25 @@ namespace WebfrontCore.Controllers public IActionResult BanForm() { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_BAN_NAME"], Name = "Ban", - Inputs = new List() + Inputs = new List { - new InputInfo() + new() { Name = "Reason", Label = Localization["WEBFRONT_ACTION_LABEL_REASON"], }, - new InputInfo() + new() { Name = "PresetReason", Type = "select", Label = Localization["WEBFRONT_ACTION_LABEL_PRESET_REASON"], Values = GetPresetPenaltyReasons() }, - new InputInfo() + new() { Name = "Duration", Label = Localization["WEBFRONT_ACTION_LABEL_DURATION"], @@ -134,7 +134,7 @@ namespace WebfrontCore.Controllers var server = Manager.GetServers().First(); - return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new + return await Task.FromResult(RedirectToAction("Execute", "Console", new { serverId = server.EndPoint, command @@ -143,13 +143,13 @@ namespace WebfrontCore.Controllers public IActionResult UnbanForm() { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_UNBAN_NAME"], Name = "Unban", - Inputs = new List() + Inputs = new List { - new InputInfo() + new() { Name = "Reason", Label = Localization["WEBFRONT_ACTION_LABEL_REASON"], @@ -162,57 +162,59 @@ namespace WebfrontCore.Controllers return View("_ActionForm", info); } - public async Task UnbanAsync(int targetId, string Reason) + public async Task UnbanAsync(int targetId, string reason) { var server = Manager.GetServers().First(); - return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new + return await Task.FromResult(RedirectToAction("Execute", "Console", new { serverId = server.EndPoint, - command = $"{_appConfig.CommandPrefix}{_unbanCommandName} @{targetId} {Reason}" + command = $"{_appConfig.CommandPrefix}{_unbanCommandName} @{targetId} {reason}" })); } public IActionResult LoginForm() { - var login = new ActionInfo() + var login = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_LOGIN_NAME"], Name = "Login", - Inputs = new List() + Inputs = new List { - new InputInfo() + new() { Name = "clientId", - Label = Localization["WEBFRONT_ACTION_LABEL_ID"] + Label = Localization["WEBFRONT_ACTION_LABEL_ID"], + Required = true }, - new InputInfo() + new() { Name = "Password", Label = Localization["WEBFRONT_ACTION_LABEL_PASSWORD"], Type = "password", + Required = true } }, - Action = "LoginAsync" + Action = "Login" }; return View("_ActionForm", login); } - public async Task LoginAsync(int clientId, string password) + public async Task Login(int clientId, string password) { - return await Task.FromResult(RedirectToAction("LoginAsync", "Account", new {clientId, password})); + return await Task.FromResult(RedirectToAction("Login", "Account", new {clientId, password})); } public IActionResult EditForm() { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_EDIT"], Name = "Edit", - Inputs = new List() + Inputs = new List { - new InputInfo() + new() { Name = "level", Label = Localization["WEBFRONT_PROFILE_LEVEL"], @@ -235,7 +237,7 @@ namespace WebfrontCore.Controllers { var server = Manager.GetServers().First(); - return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new + return await Task.FromResult(RedirectToAction("Execute", "Console", new { serverId = server.EndPoint, command = $"{_appConfig.CommandPrefix}{_setLevelCommandName} @{targetId} {level}" @@ -244,7 +246,7 @@ namespace WebfrontCore.Controllers public IActionResult GenerateLoginTokenForm() { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_GENERATE_TOKEN"], Name = "GenerateLoginToken", @@ -267,19 +269,19 @@ namespace WebfrontCore.Controllers public IActionResult ChatForm(long id) { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"], Name = "Chat", Inputs = new List { - new InputInfo() + new() { Name = "message", Type = "text", Label = Localization["WEBFRONT_ACTION_LABEL_MESSAGE"] }, - new InputInfo() + new() { Name = "id", Value = id.ToString(), @@ -294,7 +296,7 @@ namespace WebfrontCore.Controllers public async Task ChatAsync(long id, string message) { - var server = Manager.GetServers().First(_server => _server.EndPoint == id); + var server = Manager.GetServers().First(server => server.EndPoint == id); server.ChatHistory.Add(new SharedLibraryCore.Dtos.ChatInfo() { @@ -305,7 +307,7 @@ namespace WebfrontCore.Controllers Time = DateTime.Now }); - return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new + return await Task.FromResult(RedirectToAction("Execute", "Console", new { serverId = server.EndPoint, command = $"{_appConfig.CommandPrefix}{_sayCommandName} {message}" @@ -318,7 +320,7 @@ namespace WebfrontCore.Controllers foreach (var client in clients) { client.IPAddress = - _appConfig.HasPermission(Client.Level, WebfrontEntity.IPAddress, WebfrontPermission.Read) + _appConfig.HasPermission(Client.Level, WebfrontEntity.ClientIPAddress, WebfrontPermission.Read) ? client.IPAddress : null; } @@ -328,18 +330,18 @@ namespace WebfrontCore.Controllers public IActionResult FlagForm() { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_FLAG_NAME"], Name = "Flag", - Inputs = new List() + Inputs = new List { - new InputInfo() + new() { Name = "reason", Label = Localization["WEBFRONT_ACTION_LABEL_REASON"], }, - new InputInfo() + new() { Name = "PresetReason", Type = "select", @@ -358,7 +360,7 @@ namespace WebfrontCore.Controllers { var server = Manager.GetServers().First(); - return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new + return await Task.FromResult(RedirectToAction("Execute", "Console", new { serverId = server.EndPoint, command = $"{_appConfig.CommandPrefix}{_flagCommandName} @{targetId} {presetReason ?? reason}" @@ -367,13 +369,13 @@ namespace WebfrontCore.Controllers public IActionResult UnflagForm() { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_UNFLAG_NAME"], Name = "Unflag", - Inputs = new List() + Inputs = new List { - new InputInfo() + new() { Name = "reason", Label = Localization["WEBFRONT_ACTION_LABEL_REASON"], @@ -390,7 +392,7 @@ namespace WebfrontCore.Controllers { var server = Manager.GetServers().First(); - return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new + return await Task.FromResult(RedirectToAction("Execute", "Console", new { serverId = server.EndPoint, command = $"{_appConfig.CommandPrefix}{_unflagCommandName} @{targetId} {reason}" @@ -399,25 +401,25 @@ namespace WebfrontCore.Controllers public IActionResult KickForm(int id) { - var info = new ActionInfo() + var info = new ActionInfo { ActionButtonLabel = Localization["WEBFRONT_ACTION_KICK_NAME"], Name = "Kick", - Inputs = new List() + Inputs = new List { - new InputInfo() + new() { Name = "reason", Label = Localization["WEBFRONT_ACTION_LABEL_REASON"], }, - new InputInfo() + new() { Name = "PresetReason", Type = "select", Label = Localization["WEBFRONT_ACTION_LABEL_PRESET_REASON"], Values = GetPresetPenaltyReasons() }, - new InputInfo() + new() { Name = "targetId", Type = "hidden", @@ -433,14 +435,14 @@ namespace WebfrontCore.Controllers public async Task KickAsync(int targetId, string reason, string presetReason = null) { - var client = Manager.GetActiveClients().FirstOrDefault(_client => _client.ClientId == targetId); + var client = Manager.GetActiveClients().FirstOrDefault(client => client.ClientId == targetId); if (client == null) { return BadRequest(Localization["WEBFRONT_ACTION_KICK_DISCONNECT"]); } - return await Task.FromResult(RedirectToAction("ExecuteAsync", "Console", new + return await Task.FromResult(RedirectToAction("Execute", "Console", new { serverId = client.CurrentServer.EndPoint, command = $"{_appConfig.CommandPrefix}{_kickCommandName} {client.ClientNumber} {presetReason ?? reason}" @@ -449,9 +451,9 @@ namespace WebfrontCore.Controllers private Dictionary GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values .Concat(_appConfig.GlobalRules) - .Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? new string[0])) + .Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty())) .Distinct() - .Select((value, index) => new + .Select((value, _) => new { Value = value }) diff --git a/WebfrontCore/Controllers/Client/ClientController.cs b/WebfrontCore/Controllers/Client/ClientController.cs index 9a80733bd..ca97a481e 100644 --- a/WebfrontCore/Controllers/Client/ClientController.cs +++ b/WebfrontCore/Controllers/Client/ClientController.cs @@ -22,14 +22,22 @@ namespace WebfrontCore.Controllers { private readonly IMetaServiceV2 _metaService; private readonly StatsConfiguration _config; + private readonly IGeoLocationService _geoLocationService; - public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config) : base(manager) + public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config, + IGeoLocationService geoLocationService) : base(manager) { _metaService = metaService; _config = config; + _geoLocationService = geoLocationService; } - public async Task ProfileAsync(int id, MetaType? metaFilterType, CancellationToken token = default) + [Obsolete] + public IActionResult ProfileAsync(int id, MetaType? metaFilterType, + CancellationToken token = default) => RedirectToAction("Profile", "Client", new + { id, metaFilterType, token }); + + public async Task Profile(int id, MetaType? metaFilterType, CancellationToken token = default) { var client = await Manager.GetClientService().Get(id); @@ -43,7 +51,8 @@ namespace WebfrontCore.Controllers var persistentMetaTask = new[] { - _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId, token), + _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId, + token), _metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token) }; @@ -74,6 +83,7 @@ namespace WebfrontCore.Controllers } displayLevel = string.IsNullOrEmpty(client.Tag) ? displayLevel : $"{displayLevel} ({client.Tag})"; + var ingameClient = Manager.GetActiveClients().FirstOrDefault(c => c.ClientId == client.ClientId); var clientDto = new PlayerInfo { @@ -81,29 +91,38 @@ namespace WebfrontCore.Controllers Level = displayLevel, LevelInt = displayLevelInt, ClientId = client.ClientId, - IPAddress = PermissionsSet.HasPermission(WebfrontEntity.IPAddress, WebfrontPermission.Read) ? client.IPAddressString : null, + IPAddress = PermissionsSet.HasPermission(WebfrontEntity.ClientIPAddress, WebfrontPermission.Read) + ? client.IPAddressString + : null, NetworkId = client.NetworkId, Meta = new List(), Aliases = client.AliasLink.Children - .Select(_alias => _alias.Name) - .GroupBy(_alias => _alias.StripColors()) + .Select(alias => (alias.Name, alias.DateAdded)) + .GroupBy(alias => alias.Name.StripColors()) // we want the longest "duplicate" name - .Select(_grp => _grp.OrderByDescending(_name => _name.Length).First()) + .Select(grp => grp.OrderByDescending(item => item.Name.Length).First()) .Distinct() - .OrderBy(a => a) .ToList(), - IPs = PermissionsSet.HasPermission(WebfrontEntity.IPAddress, WebfrontPermission.Read) ? client.AliasLink.Children - .Where(i => i.IPAddress != null) - .OrderByDescending(i => i.DateAdded) - .Select(i => i.IPAddress.ConvertIPtoString()) - .Prepend(client.CurrentAlias.IPAddress.ConvertIPtoString()) - .Distinct() - .ToList() : new List(), - HasActivePenalty = activePenalties.Any(_penalty => _penalty.Type != EFPenalty.PenaltyType.Flag), - Online = Manager.GetActiveClients().FirstOrDefault(c => c.ClientId == client.ClientId) != null, + IPs = PermissionsSet.HasPermission(WebfrontEntity.ClientIPAddress, WebfrontPermission.Read) + ? client.AliasLink.Children + .Select(alias => (alias.IPAddress.ConvertIPtoString(), alias.DateAdded)) + .GroupBy(alias => alias.Item1) + .Select(grp => grp.OrderByDescending(item => item.DateAdded).First()) + .Distinct() + .ToList() + : new List<(string, DateTime)>(), + HasActivePenalty = activePenalties.Any(penalty => penalty.Type != EFPenalty.PenaltyType.Flag), + Online = ingameClient != null, TimeOnline = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(), LinkedAccounts = client.LinkedAccounts, - MetaFilterType = metaFilterType + MetaFilterType = metaFilterType, + ConnectProtocolUrl = ingameClient?.CurrentServer.EventParser.URLProtocolFormat.FormatExt( + ingameClient.CurrentServer.ResolvedIpEndPoint.Address.IsInternal() + ? Program.Manager.ExternalIPAddress + : ingameClient.CurrentServer.IP, + ingameClient.CurrentServer.Port), + CurrentServerName = ingameClient?.CurrentServer?.Hostname, + GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString) }; var meta = await _metaService.GetRuntimeMeta(new ClientPaginationRequest @@ -126,9 +145,9 @@ namespace WebfrontCore.Controllers clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive)); var strippedName = clientDto.Name.StripColors(); - ViewBag.Title = strippedName.Substring(strippedName.Length - 1).ToLower()[0] == 's' ? - strippedName + "'" : - strippedName + "'s"; + ViewBag.Title = strippedName.Substring(strippedName.Length - 1).ToLower()[0] == 's' + ? strippedName + "'" + : strippedName + "'s"; ViewBag.Title += " " + Localization["WEBFRONT_CLIENT_PROFILE_TITLE"]; ViewBag.Description = $"Client information for {strippedName}"; ViewBag.Keywords = $"IW4MAdmin, client, profile, {strippedName}"; @@ -137,13 +156,13 @@ namespace WebfrontCore.Controllers return View("Profile/Index", clientDto); } - public async Task PrivilegedAsync() + public async Task Privileged() { if (Manager.GetApplicationSettings().Configuration().EnablePrivilegedUserPrivacy && !Authorized) { return RedirectToAction("Index", "Home"); } - + var admins = (await Manager.GetClientService().GetPrivilegedClients()) .OrderByDescending(_client => _client.Level) .ThenBy(_client => _client.Name); @@ -160,7 +179,8 @@ namespace WebfrontCore.Controllers adminsDict[admin.Level].Add(new ClientInfo() { Name = admin.Name, - ClientId = admin.ClientId + ClientId = admin.ClientId, + LastConnection = admin.LastConnection }); } @@ -171,7 +191,7 @@ namespace WebfrontCore.Controllers return View("Privileged/Index", adminsDict); } - public async Task FindAsync(string clientName) + public async Task Find(string clientName) { if (string.IsNullOrWhiteSpace(clientName)) { @@ -189,11 +209,13 @@ namespace WebfrontCore.Controllers } } - ViewBag.Title = $"{clientsDto.Count} {Localization["WEBFRONT_CLIENT_SEARCH_MATCHING"]} \"{clientName}\""; + ViewBag.SearchTerm = clientName; + ViewBag.ResultCount = clientsDto.Count; return View("Find/Index", clientsDto); } - public IActionResult Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType, CancellationToken token) + public IActionResult Meta(int id, int count, int offset, long? startAt, MetaType? metaFilterType, + CancellationToken token) { var request = new ClientPaginationRequest { diff --git a/WebfrontCore/Controllers/Client/Legacy/StatsController.cs b/WebfrontCore/Controllers/Client/Legacy/StatsController.cs index a84d25a38..18847620c 100644 --- a/WebfrontCore/Controllers/Client/Legacy/StatsController.cs +++ b/WebfrontCore/Controllers/Client/Legacy/StatsController.cs @@ -42,15 +42,20 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers } [HttpGet] - public IActionResult TopPlayersAsync() + public IActionResult TopPlayers(string serverId = null) { ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"]; ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"]; - ViewBag.Servers = _manager.GetServers() - .Select(_server => new ServerInfo() {Name = _server.Hostname, ID = _server.EndPoint}); ViewBag.Localization = _translationLookup; + ViewBag.SelectedServerId = serverId; - return View("~/Views/Client/Statistics/Index.cshtml"); + return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers() + .Select(server => new ServerInfo + { + Name = server.Hostname, + IPAddress = server.IP, + Port = server.Port + })); } [HttpGet] @@ -169,7 +174,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers var penalty = await context.Penalties .Select(_penalty => new - {_penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense}) + { _penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense }) .FirstOrDefaultAsync(_penalty => _penalty.PenaltyId == penaltyId); if (penalty == null) diff --git a/WebfrontCore/Controllers/ConfigurationController.cs b/WebfrontCore/Controllers/ConfigurationController.cs index 5eee32445..784f37037 100644 --- a/WebfrontCore/Controllers/ConfigurationController.cs +++ b/WebfrontCore/Controllers/ConfigurationController.cs @@ -39,7 +39,7 @@ namespace WebfrontCore.Controllers return Unauthorized(); } - return View("Index", Manager.GetApplicationSettings().Configuration()); + return RedirectToAction("Files"); } public async Task Files() @@ -256,4 +256,4 @@ namespace WebfrontCore.Controllers .Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore)) .FirstOrDefault() as ConfigurationIgnore) != null; } -} \ No newline at end of file +} diff --git a/WebfrontCore/Controllers/ConsoleController.cs b/WebfrontCore/Controllers/ConsoleController.cs index 910d8223f..e43290324 100644 --- a/WebfrontCore/Controllers/ConsoleController.cs +++ b/WebfrontCore/Controllers/ConsoleController.cs @@ -34,11 +34,11 @@ namespace WebfrontCore.Controllers return View(activeServers); } - public async Task ExecuteAsync(long serverId, string command) + public async Task Execute(long serverId, string command) { var server = Manager.GetServers().First(s => s.EndPoint == serverId); - var client = new EFClient() + var client = new EFClient { ClientId = Client.ClientId, Level = Client.Level, @@ -50,7 +50,7 @@ namespace WebfrontCore.Controllers } }; - var remoteEvent = new GameEvent() + var remoteEvent = new GameEvent { Type = GameEvent.EventType.Command, Data = command.StartsWith(_appconfig.CommandPrefix) || @@ -97,15 +97,15 @@ namespace WebfrontCore.Controllers { response = new[] { - new CommandResponseInfo() + new CommandResponseInfo { ClientId = client.ClientId, Response = Utilities.CurrentLocalization.LocalizationIndex["COMMADS_RESTART_SUCCESS"] } }; } - - return View("_Response", response); + + return remoteEvent.Failed ? StatusCode(400, response) : Ok(response); } } -} \ No newline at end of file +} diff --git a/WebfrontCore/Controllers/PenaltyController.cs b/WebfrontCore/Controllers/PenaltyController.cs index 944cd88f9..332380d03 100644 --- a/WebfrontCore/Controllers/PenaltyController.cs +++ b/WebfrontCore/Controllers/PenaltyController.cs @@ -30,49 +30,15 @@ namespace WebfrontCore.Controllers return View(showOnly); } - public async Task ListAsync(int offset = 0, EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true) + public async Task ListAsync(int offset = 0, int count = 30, EFPenalty.PenaltyType showOnly = EFPenalty.PenaltyType.Any, bool hideAutomatedPenalties = true) { - return await Task.FromResult(View("_List", new ViewModels.PenaltyFilterInfo() + return await Task.FromResult(View("_List", new ViewModels.PenaltyFilterInfo { Offset = offset, + Count = count, ShowOnly = showOnly, IgnoreAutomated = hideAutomatedPenalties })); } - - /// - /// retrieves all permanent bans ordered by ban date - /// if request is authorized, it will include the client's ip address. - /// - /// - public async Task PublicAsync() - { - IList penalties; - - await using var ctx = _contextFactory.CreateContext(false); - var iqPenalties = ctx.Penalties - .AsNoTracking() - .Where(p => p.Type == EFPenalty.PenaltyType.Ban && p.Active) - .OrderByDescending(_penalty => _penalty.When) - .Select(p => new PenaltyInfo() - { - Id = p.PenaltyId, - OffenderId = p.OffenderId, - OffenderName = p.Offender.CurrentAlias.Name, - OffenderNetworkId = (ulong)p.Offender.NetworkId, - OffenderIPAddress = Authorized ? p.Offender.CurrentAlias.IPAddress.ConvertIPtoString() : null, - Offense = p.Offense, - PunisherId = p.PunisherId, - PunisherNetworkId = (ulong)p.Punisher.NetworkId, - PunisherName = p.Punisher.CurrentAlias.Name, - PunisherIPAddress = Authorized ? p.Punisher.CurrentAlias.IPAddress.ConvertIPtoString() : null, - TimePunished = p.When, - AutomatedOffense = Authorized ? p.AutomatedOffense : null, - }); - - penalties = await iqPenalties.ToListAsync(); - - return Json(penalties); - } } } diff --git a/WebfrontCore/Controllers/ServerController.cs b/WebfrontCore/Controllers/ServerController.cs index 260a8e119..0f1532e52 100644 --- a/WebfrontCore/Controllers/ServerController.cs +++ b/WebfrontCore/Controllers/ServerController.cs @@ -28,7 +28,7 @@ namespace WebfrontCore.Controllers return NotFound(); } - var serverInfo = new ServerInfo() + var serverInfo = new ServerInfo { Name = s.Hostname, ID = s.EndPoint, @@ -44,7 +44,7 @@ namespace WebfrontCore.Controllers ClientId = p.ClientId, Level = p.Level.ToLocalizedLevelName(), LevelInt = (int) p.Level, - ZScore = p.GetAdditionalProperty(IW4MAdmin.Plugins.Stats.Helpers.StatManager + ZScore = p.GetAdditionalProperty(StatManager .CLIENT_STATS_KEY)?.ZScore }).ToList(), ChatHistory = s.ChatHistory.ToList(), @@ -55,37 +55,40 @@ namespace WebfrontCore.Controllers } [HttpGet] - public ActionResult Scoreboard() + public ActionResult Scoreboard(string serverId) { ViewBag.Title = Localization["WEBFRONT_TITLE_SCOREBOARD"]; + ViewBag.SelectedServerId = string.IsNullOrEmpty(serverId) ? Manager.GetServers().FirstOrDefault()?.ToString() : serverId; - return View(ProjectScoreboard(Manager.GetServers(), null, true)); + return View(ProjectScoreboard(Manager.GetServers(), null, true, false)); } [HttpGet("[controller]/{id}/scoreboard")] - public ActionResult Scoreboard(long id, [FromQuery]string order = null, [FromQuery] bool down = true) + public ActionResult Scoreboard(string id, [FromQuery]string order = null, [FromQuery] bool down = true) { - var server = Manager.GetServers().FirstOrDefault(srv => srv.EndPoint == id); + + var server = Manager.GetServers().FirstOrDefault(srv => srv.ToString() == id); if (server == null) { return NotFound(); } + ViewBag.SelectedServerId = id; return View("_Scoreboard", ProjectScoreboard(new[] {server}, order, down).First()); } private static IEnumerable ProjectScoreboard(IEnumerable servers, string order, - bool down) + bool down, bool includeDetails = true) { - return servers.Select(server => new ScoreboardInfo + return servers.Select((server, index) => new ScoreboardInfo { OrderByKey = order, ShouldOrderDescending = down, MapName = server.CurrentMap.ToString(), ServerName = server.Hostname, - ServerId = server.EndPoint, - ClientInfo = server.GetClientsAsList().Select(client => + ServerId = server.ToString(), + ClientInfo = index == 0 && !includeDetails || includeDetails ? server.GetClientsAsList().Select(client => new { stats = client.GetAdditionalProperty(StatManager.CLIENT_STATS_KEY), @@ -104,7 +107,7 @@ namespace WebfrontCore.Controllers ZScore = clientData.stats?.ZScore == null || clientData.stats.ZScore == 0 ? null : clientData.stats.ZScore, Team = clientData.client.Team }) - .ToList() + .ToList() : new List() }).ToList(); } } diff --git a/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs b/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs index 3d9dfb179..20e75dcca 100644 --- a/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs +++ b/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Data.Models.Client; +using Microsoft.AspNetCore.Authentication.Cookies; using static SharedLibraryCore.GameEvent; namespace WebfrontCore.Middleware @@ -81,7 +82,7 @@ namespace WebfrontCore.Middleware // they've been removed if (!_privilegedClientIds.Contains(clientId) && clientId != 1) { - await context.SignOutAsync(); + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } } diff --git a/WebfrontCore/Permissions/WebfrontEntity.cs b/WebfrontCore/Permissions/WebfrontEntity.cs index a7167b6c0..c9bd4558c 100644 --- a/WebfrontCore/Permissions/WebfrontEntity.cs +++ b/WebfrontCore/Permissions/WebfrontEntity.cs @@ -2,8 +2,19 @@ public enum WebfrontEntity { - IPAddress, - MetaAliasUpdate + ClientIPAddress, + ClientGuid, + ClientLevel, + MetaAliasUpdate, + Penalty, + PrivilegedClientsPage, + HelpPage, + ConsolePage, + ConfigurationPage, + AuditPage, + RecentPlayersPage, + ProfilePage, + AdminMenu } public enum WebfrontPermission diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index 2d434ca3b..723945c95 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -131,6 +131,8 @@ namespace WebfrontCore Program.ApplicationServiceProvider.GetRequiredService()); services.AddSingleton(Program.ApplicationServiceProvider .GetRequiredService>()); + services.AddSingleton(Program.ApplicationServiceProvider + .GetRequiredService()); services.AddSingleton(Program.ApplicationServiceProvider .GetRequiredService()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService()); diff --git a/WebfrontCore/TagHelpers/HasPermission.cs b/WebfrontCore/TagHelpers/HasPermission.cs new file mode 100644 index 000000000..168fd59c9 --- /dev/null +++ b/WebfrontCore/TagHelpers/HasPermission.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Razor.TagHelpers; +using SharedLibraryCore; +using SharedLibraryCore.Configuration; +using WebfrontCore.Permissions; + +namespace WebfrontCore.TagHelpers; + +[HtmlTargetElement("has-permission")] +public class HasPermission : TagHelper +{ + [HtmlAttributeName("entity")] public WebfrontEntity Entity { get; set; } + + [HtmlAttributeName("required-permission")] + public WebfrontPermission Permission { get; set; } + + private readonly IDictionary> _permissionSets; + private readonly IHttpContextAccessor _contextAccessor; + + public HasPermission(ApplicationConfiguration appConfig, IHttpContextAccessor contextAccessor) + { + _permissionSets = appConfig.PermissionSets; + _contextAccessor = contextAccessor; + } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = null; + var permissionLevel = _contextAccessor?.HttpContext?.User.Claims + .FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value; + + var hasPermission = permissionLevel != null && _permissionSets.ContainsKey(permissionLevel) && + _permissionSets[permissionLevel].HasPermission(Entity, Permission); + if (!hasPermission) + { + output.SuppressOutput(); + } + } +} diff --git a/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs b/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs index 829656d3e..ea1b22307 100644 --- a/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs +++ b/WebfrontCore/ViewComponents/PenaltyListViewComponent.cs @@ -7,11 +7,9 @@ namespace WebfrontCore.ViewComponents { public class PenaltyListViewComponent : ViewComponent { - private const int PENALTY_COUNT = 15; - - public async Task InvokeAsync(int offset, EFPenalty.PenaltyType showOnly, bool ignoreAutomated) + public async Task InvokeAsync(int offset, int count, EFPenalty.PenaltyType showOnly, bool ignoreAutomated) { - var penalties = await Program.Manager.GetPenaltyService().GetRecentPenalties(PENALTY_COUNT, offset, showOnly, ignoreAutomated); + var penalties = await Program.Manager.GetPenaltyService().GetRecentPenalties(count, offset, showOnly, ignoreAutomated); penalties = User.Identity.IsAuthenticated ? penalties : penalties.Where(p => !p.Sensitive).ToList(); return View("~/Views/Penalty/PenaltyInfoList.cshtml", penalties); diff --git a/WebfrontCore/ViewComponents/ServerListViewComponent.cs b/WebfrontCore/ViewComponents/ServerListViewComponent.cs index b4f662a2d..0fad09f33 100644 --- a/WebfrontCore/ViewComponents/ServerListViewComponent.cs +++ b/WebfrontCore/ViewComponents/ServerListViewComponent.cs @@ -94,8 +94,8 @@ namespace WebfrontCore.ViewComponents }).ToList(), ChatHistory = server.ChatHistory.ToList(), Online = !server.Throttled, - IPAddress = - $"{(server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP)}:{server.Port}", + IPAddress = server.IP, + ExternalIPAddress = server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP, ConnectProtocolUrl = server.EventParser.URLProtocolFormat.FormatExt( server.ResolvedIpEndPoint.Address.IsInternal() ? Program.Manager.ExternalIPAddress : server.IP, server.Port) diff --git a/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs b/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs index 3a1adc119..63934ab49 100644 --- a/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs +++ b/WebfrontCore/ViewComponents/TopPlayersViewComponent.cs @@ -16,22 +16,16 @@ namespace WebfrontCore.ViewComponents _config = config; } - public async Task InvokeAsync(int count, int offset, long? serverId = null) + public async Task InvokeAsync(int count, int offset, string serverEndpoint = null) { - if (serverId == 0) - { - serverId = null; - } - - var server = Plugin.ServerManager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId); - - if (server != null) - { - serverId = StatManager.GetIdForServer(server); - } + var server = Plugin.ServerManager.GetServers() + .FirstOrDefault(server => server.ToString() == serverEndpoint); + var serverId = server is null ? (long?)null : StatManager.GetIdForServer(server); ViewBag.UseNewStats = _config?.EnableAdvancedMetrics ?? true; + ViewBag.SelectedServerName = server?.Hostname; + return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml", ViewBag.UseNewStats ? await Plugin.Manager.GetNewTopStats(offset, count, serverId) diff --git a/WebfrontCore/ViewModels/InputInfo.cs b/WebfrontCore/ViewModels/InputInfo.cs index 41de4d0be..bf0227eb5 100644 --- a/WebfrontCore/ViewModels/InputInfo.cs +++ b/WebfrontCore/ViewModels/InputInfo.cs @@ -14,5 +14,6 @@ namespace WebfrontCore.ViewModels public string Value { get; set; } public Dictionary Values { get; set; } public bool Checked { get; set; } + public bool Required { get; set; } } } diff --git a/WebfrontCore/ViewModels/PenaltyFilterInfo.cs b/WebfrontCore/ViewModels/PenaltyFilterInfo.cs index 81ef8a7c9..bd947c600 100644 --- a/WebfrontCore/ViewModels/PenaltyFilterInfo.cs +++ b/WebfrontCore/ViewModels/PenaltyFilterInfo.cs @@ -11,6 +11,8 @@ namespace WebfrontCore.ViewModels /// number of items offset from the start of the list /// public int Offset { get; set; } + + public int Count { get; set; } /// /// show only a certain type of penalty diff --git a/WebfrontCore/ViewModels/ScoreboardInfo.cs b/WebfrontCore/ViewModels/ScoreboardInfo.cs index 1e5d3570e..59a2f4bd2 100644 --- a/WebfrontCore/ViewModels/ScoreboardInfo.cs +++ b/WebfrontCore/ViewModels/ScoreboardInfo.cs @@ -3,17 +3,16 @@ using SharedLibraryCore.Database.Models; namespace WebfrontCore.ViewModels { - public class ScoreboardInfo { - public string ServerName { get; set; } - public long ServerId { get; set; } - public string MapName { get; set; } - public string OrderByKey { get; set; } - public bool ShouldOrderDescending { get; set; } - public List ClientInfo { get; set; } + public string ServerName { get; set; } + public string ServerId { get; set; } + public string MapName { get; set; } + public string OrderByKey { get; set; } + public bool ShouldOrderDescending { get; set; } + public List ClientInfo { get; set; } } - + public class ClientScoreboardInfo { public string ClientName { get; set; } diff --git a/WebfrontCore/ViewModels/SideContextMenuItem.cs b/WebfrontCore/ViewModels/SideContextMenuItem.cs new file mode 100644 index 000000000..e583fb335 --- /dev/null +++ b/WebfrontCore/ViewModels/SideContextMenuItem.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace WebfrontCore.ViewModels; + +public class SideContextMenuItem +{ + public bool IsLink { get; set; } + public bool IsButton { get; set; } + public bool IsActive { get; set; } + public string Title { get; set; } + public string Reference { get; set; } + public string Icon { get; set; } + public string Tooltip { get; set; } +} + + +public class SideContextMenuItems +{ + public string MenuTitle { get; set; } + public List Items { get; set; } = new(); +} diff --git a/WebfrontCore/ViewModels/TableInfo.cs b/WebfrontCore/ViewModels/TableInfo.cs new file mode 100644 index 000000000..86fa48da1 --- /dev/null +++ b/WebfrontCore/ViewModels/TableInfo.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace WebfrontCore.ViewModels; + +public class TableInfo +{ + public string Header { get; set; } + public List Columns { get; } = new(); + public List Rows { get; } = new(); + public int InitialRowCount { get; } + + public TableInfo(int initialRowCount = 0) + { + InitialRowCount = initialRowCount; + } +} + +public class RowDefinition +{ + public List Datum { get; } = new(); +} + +public class ColumnDefinition +{ + public string Title { get; set; } + public string ColumnSpan { get; set; } +} + +public static class TableInfoExtensions +{ + public static TableInfo WithColumns(this TableInfo info, IEnumerable columns) + { + info.Columns.AddRange(columns.Select(column => new ColumnDefinition + { + Title = column + })); + + return info; + } + + public static TableInfo WithRows(this TableInfo info, IEnumerable source, + Func> selector) + { + info.Rows.AddRange(source.Select(row => + { + var rowDef = new RowDefinition(); + rowDef.Datum.AddRange(selector(row)); + return rowDef; + })); + return info; + } +} diff --git a/WebfrontCore/Views/About/Index.cshtml b/WebfrontCore/Views/About/Index.cshtml index 1031bb74d..89bf21a30 100644 --- a/WebfrontCore/Views/About/Index.cshtml +++ b/WebfrontCore/Views/About/Index.cshtml @@ -2,7 +2,7 @@ @using System.Text.RegularExpressions @model WebfrontCore.ViewModels.CommunityInfo @{ - IEnumerable> allRules = new[] {new KeyValuePair<(string, long), string[]>((ViewBag.Localization["WEBFRONT_ABOUT_GLOBAL_RULES"], 0), Model.GlobalRules)}; + IEnumerable> allRules = new[] { new KeyValuePair<(string, long), string[]>((ViewBag.Localization["WEBFRONT_ABOUT_GLOBAL_RULES"], 0), Model.GlobalRules) }; var serverRules = Model.ServerRules?.Where(server => server.Value != null && server.Value.Any()).ToList(); if (serverRules?.Any() ?? false) { @@ -10,26 +10,29 @@ } } -
+
@if (Model.CommunityInformation.EnableBanner) { - @Model.CommunityInformation.Name + @Model.CommunityInformation.Name } @if (!string.IsNullOrWhiteSpace(Model.CommunityInformation.Name)) { -

+

} - @if (!string.IsNullOrWhiteSpace(Model.CommunityInformation.Description)) - { -
-

@ViewBag.Localization["WEBFRONT_ABOUT_TITLE"]

- -
- @foreach (var social in Model.CommunityInformation.SocialAccounts ?? new SocialAccountConfiguration[0]) + + - } + } + +
@if (allRules.Any(rule => rule.Value.Any())) { -

@ViewBag.Localization["WEBFRONT_ABOUT_COMMUNITY_GUIDELINES"]

+

@ViewBag.Localization["WEBFRONT_ABOUT_COMMUNITY_GUIDELINES"]

} - @foreach (var ((serverName, id), rules) in allRules) - { - if (!rules.Any()) +
+ @foreach (var ((serverName, id), rules) in allRules) { - continue; - } + if (!rules.Any()) + { + continue; + } - var start = 1; -
-
+ var start = 1; +
-
+ @foreach (var rule in rules) { -
+
@if (!rule.StartsWith("#") && !Regex.IsMatch(rule, @"^\d+(.|\))")) { - @start. + @start. } - + + +
start++; } -
- } + } +
diff --git a/WebfrontCore/Views/Action/_ActionForm.cshtml b/WebfrontCore/Views/Action/_ActionForm.cshtml index 3c61cb420..961041aae 100644 --- a/WebfrontCore/Views/Action/_ActionForm.cshtml +++ b/WebfrontCore/Views/Action/_ActionForm.cshtml @@ -1,16 +1,22 @@ -@model WebfrontCore.ViewModels.ActionInfo +@using Humanizer +@model WebfrontCore.ViewModels.ActionInfo @{ Layout = null; } + +@if (Model.Inputs.Any()) +{ +
+}
@foreach (var input in Model.Inputs) { - string inputType = input.Type ?? "text"; - string value = input.Value ?? ""; + var inputType = input.Type ?? "text"; + var value = input.Value ?? ""; if (input.Type != "hidden") { -
+
@input.Label @@ -21,7 +27,9 @@ } @@ -37,7 +45,7 @@ else { - + }
@@ -47,5 +55,12 @@ } } - + @if (Model.Inputs.Any()) + { +
+ } +
diff --git a/WebfrontCore/Views/Admin/AuditLog.cshtml b/WebfrontCore/Views/Admin/AuditLog.cshtml index 87a0ef8b8..2e44516cd 100644 --- a/WebfrontCore/Views/Admin/AuditLog.cshtml +++ b/WebfrontCore/Views/Admin/AuditLog.cshtml @@ -1,26 +1,26 @@ @{ var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; } -

@ViewBag.Title

+
+

@ViewBag.Title

- - - +
+ + - - - - - - -
@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"] @loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"] @loc["WEBFRONT_PENALTY_TEMPLATE_NAME"] @loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"] @loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"] @loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]
- + + + + + + +
@section scripts { diff --git a/WebfrontCore/Views/Admin/_ListAuditLog.cshtml b/WebfrontCore/Views/Admin/_ListAuditLog.cshtml index 0204c2f68..ad005f8a5 100644 --- a/WebfrontCore/Views/Admin/_ListAuditLog.cshtml +++ b/WebfrontCore/Views/Admin/_ListAuditLog.cshtml @@ -1,99 +1,71 @@ @using SharedLibraryCore.Dtos @model IEnumerable @{ - var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; + var loc = Utilities.CurrentLocalization.LocalizationIndex; } @foreach (var info in Model) { - - - @loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"] - - @info.Action - - - - @loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"] - - - - - - - - @loc["WEBFRONT_PENALTY_TEMPLATE_NAME"] - - @if (info.TargetId != null) - { - - - - } - else - { - -- - } - - - - @loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"] - - @info.Data - - - @* - @loc["WEBFRONT_ADMIN_AUDIT_LOG_PREVIOUS"] - - @info.OldValue - - *@ - - @loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"] - - @info.NewValue - - - - @loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"] - - @info.When.ToString() - - - - - + + @info.Action - + @if (info.TargetId != null) { - + } else { - -- + } - - @info.Data - - @* - @info.OldValue - *@ - + + @info.Data + @info.NewValue - + @info.When.ToString() + + + + +
@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]
+
@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]
+
@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]
+
@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]
+
@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]
+
@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]
+ + +
@info.Action
+ + + + @if (info.TargetId != null) + { + + + + } + else + { +
+ } +
@info.Data
+
@info.NewValue
+
@info.When.ToString()
+ + } diff --git a/WebfrontCore/Views/Client/Find/Index.cshtml b/WebfrontCore/Views/Client/Find/Index.cshtml index d301f0310..c968c8bcd 100644 --- a/WebfrontCore/Views/Client/Find/Index.cshtml +++ b/WebfrontCore/Views/Client/Find/Index.cshtml @@ -3,60 +3,71 @@ var loc = Utilities.CurrentLocalization.LocalizationIndex; } -
-

@ViewBag.Title

-
-
-
@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]
-
@loc["WEBFRONT_PROFILE_LEVEL"]
-
@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]
-
+ +
+

@ViewBag.ResultCount result(s) for @ViewBag.SearchTerm

+ + + + + + + + + @foreach (var client in Model) { -
-
- +
+ @if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy) { -
@loc["GLOBAL_PERMISSION_USER"]
+
} else { -
@client.Level
+
} -
@client.LastConnectionText
- +
+ } - - -
-
@ViewBag.Title
- @foreach (var client in Model) - { -
-
@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]
-
@loc["WEBFRONT_PROFILE_LEVEL"]
-
@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]
-
-
- - @if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy) - { -
@loc["GLOBAL_PERMISSION_USER"]
- } - else - { -
@client.Level
- } -
@client.LastConnectionText
-
- } +
+
@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]@loc["WEBFRONT_PROFILE_LEVEL"]@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]
+ - + @loc["GLOBAL_PERMISSION_USER"]@client.Level@client.LastConnection.HumanizeForCurrentCulture()
+ + + + + + @foreach (var client in Model) + { + + + + + } + +
+
@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]
+
@loc["WEBFRONT_PROFILE_LEVEL"]
+
@loc["WEBFRONT_SEARCH_LAST_CONNECTED"]
+
+ + + + @if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy) + { +
@loc["GLOBAL_PERMISSION_USER"]
+ } + else + { +
@client.Level
+ } +
@client.LastConnection.HumanizeForCurrentCulture()
+
+ +
diff --git a/WebfrontCore/Views/Client/Message/Find.cshtml b/WebfrontCore/Views/Client/Message/Find.cshtml index eeb965a34..64fcf098f 100644 --- a/WebfrontCore/Views/Client/Message/Find.cshtml +++ b/WebfrontCore/Views/Client/Message/Find.cshtml @@ -1,30 +1,33 @@ @using SharedLibraryCore.Dtos.Meta.Responses @model SharedLibraryCore.Helpers.ResourceQueryHelperResult +
@if (ViewBag.Error != null) { -

@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)

+

@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)

} else { -

@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], Model.TotalResultCount.ToString("N0"))

+

@Html.Raw(Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], $"{Model.TotalResultCount.ToString("N0")}"))

- +
- - - - - + + + + + - +
@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]
@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]
- +
+ +
@section scripts { @@ -36,4 +39,5 @@ else }); } -} \ No newline at end of file +} +
diff --git a/WebfrontCore/Views/Client/Message/_Item.cshtml b/WebfrontCore/Views/Client/Message/_Item.cshtml index 67deebb34..90d48043c 100644 --- a/WebfrontCore/Views/Client/Message/_Item.cshtml +++ b/WebfrontCore/Views/Client/Message/_Item.cshtml @@ -5,64 +5,56 @@ { - - + + - + @if (message.IsHidden && !ViewBag.Authorized) { - + } else { } - + - + @message.When - - @ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"] - - + + +
@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]
+
@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]
+
@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]
+
@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]
+ + +
- - +
+ @if (message.IsHidden && !ViewBag.Authorized) + { + + } + else + { + + } +
+
+ +
+
@message.When
- - @ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"] - - @if (message.IsHidden && !ViewBag.Authorized) - { - - } - else - { - - } - - - - - @ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"] - - - - - - - @ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"] - - @message.When } diff --git a/WebfrontCore/Views/Client/Privileged/Index.cshtml b/WebfrontCore/Views/Client/Privileged/Index.cshtml index 35204ead0..e1eb61cad 100644 --- a/WebfrontCore/Views/Client/Privileged/Index.cshtml +++ b/WebfrontCore/Views/Client/Privileged/Index.cshtml @@ -1,24 +1,31 @@ @model Dictionary> +
+

@ViewBag.Title

-

@ViewBag.Title

- -
- @{ - foreach (var key in Model.Keys) - { -
- @Utilities.ToLocalizedLevelName(key) -
- -
- @foreach (var client in Model[key]) - { - - - -
- } -
- } + @foreach (var key in Model.Keys) + { + + + + + + + + + @foreach (var client in Model[key].OrderByDescending(client => client.LastConnection)) + { + + + + + } + +
@key.ToLocalizedLevelName()Last Connected
+ + + + + @client.LastConnection.HumanizeForCurrentCulture() +
}
diff --git a/WebfrontCore/Views/Client/Profile/Index.cshtml b/WebfrontCore/Views/Client/Profile/Index.cshtml index 90397bd96..5d5403e1c 100644 --- a/WebfrontCore/Views/Client/Profile/Index.cshtml +++ b/WebfrontCore/Views/Client/Profile/Index.cshtml @@ -1,6 +1,8 @@ @using SharedLibraryCore.Interfaces @using Data.Models @using Data.Models.Client +@using WebfrontCore.Permissions +@using WebfrontCore.ViewModels @model SharedLibraryCore.Dtos.PlayerInfo @{ var match = System.Text.RegularExpressions.Regex.Match(Model.Name.ToUpper(), "[A-Z]").Value; @@ -11,70 +13,39 @@ var isTempBanned = Model.ActivePenalty?.Type == EFPenalty.PenaltyType.TempBan; var translationKey = $"WEBFRONT_PROFILE_{Model.ActivePenalty?.Type.ToString().ToUpper()}_INFO"; var ignoredMetaTypes = new[] { MetaType.Information, MetaType.Other, MetaType.QuickMessage }; + + string ClassForPenaltyType(EFPenalty.PenaltyType type) + { + return type switch + { + EFPenalty.PenaltyType.Ban => "alert-danger", + EFPenalty.PenaltyType.Flag => "alert-secondary", + EFPenalty.PenaltyType.TempBan => "alert-secondary", + _ => "alert" + }; + } + + string ClassForProfileBackground() + { + return (ViewBag.PermissionsSet as IEnumerable).HasPermission(WebfrontEntity.ClientLevel, WebfrontPermission.Read) ? $"level-bgcolor-{Model.LevelInt}" : "level-bgcolor-0"; + } } -
- -
- @if (string.IsNullOrEmpty(gravatarUrl)) - { - @shortCode - } -
- -
-
-
- -
- @if (ViewBag.Authorized) - { -
- } -
- - @if (ViewBag.Authorized) - { -
-
-
-
- -
- @foreach (var linked in Model.LinkedAccounts) - { -
- @Html.ActionLink(linked.Value.ToString("X"), "ProfileAsync", "Client", new { id = linked.Key }, new { @class = "link-inverse" }) -
- } - @foreach (var alias in Model.Aliases) - { -
- -
- } - - @foreach (var ip in Model.IPs) - { -
- @ip - -
- } -
- } - @if (Model.ActivePenalty != null && (Model.ActivePenalty.Type != EFPenalty.PenaltyType.Flag || ViewBag.Authorized)) - { -
+
+
+ @if (Model.ActivePenalty != null) + { + + - } - else + + } + +

Player Profile

+ +
+ + + @if (Model.Online) { - if (!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy) - { -
- @ViewBag.Localization["GLOBAL_PERMISSION_USER"] +
+
+ } + + +
+
+
+ @if (string.IsNullOrEmpty(gravatarUrl)) + { +
@shortCode
+ }
- } +
+ +
+ + + + + + + +
+
+ @Model.Level +
+
+
+ + + + + + + +
+ @Model.IPAddress + + + +
- } - } -
+
-
- @if (ViewBag.Authorized) - { - @if (!isPermBanned) - { - - } - - @if (Model.LevelInt < (int)ViewBag.User.Level && !Model.HasActivePenalty) - { - - } - - @if (Model.LevelInt < (int)ViewBag.User.Level && Model.HasActivePenalty) - { - @if (isTempBanned) - { - - - } - else - { - - } - } - - - @if (Model.LevelInt != -1) - { - - } - } - @if (ViewBag.UseNewStats) - { - - } -
-
- -
- -
- -
-
- -
- @{ - const int defaultTabCount = 5; - var metaTypes = Enum.GetValues(typeof(MetaType)) - .Cast() - .Where(type => !ignoredMetaTypes.Contains(type)) - .OrderByDescending(type => type == MetaType.All) - .ToList(); - var selectedMeta = metaTypes.FirstOrDefault(meta => metaTypes.IndexOf(Model.MetaFilterType ?? MetaType.All) >= defaultTabCount && meta != MetaType.All && meta == Model.MetaFilterType); - } - @foreach (var type in metaTypes.Take(defaultTabCount - 1).Append(selectedMeta == MetaType.Other ? metaTypes[defaultTabCount - 1] : selectedMeta)) - { - - @type.ToTranslatedName() - - } -
- @foreach (var type in (selectedMeta == MetaType.Other ? metaTypes.Skip(defaultTabCount) : metaTypes.Skip(defaultTabCount).Append(metaTypes[defaultTabCount - 1])).Where(meta => selectedMeta == MetaType.Other || meta != selectedMeta)) - { - - @type.ToTranslatedName() +
+ +
+ + +
+ +
+ +
+ + +
+ @foreach (var type in Enum.GetValues(typeof(MetaType)).Cast().Where(meta => !ignoredMetaTypes.Contains(meta)).OrderByDescending(meta => meta == MetaType.All)) + { + var buttonClass = !Model.MetaFilterType.HasValue && type == MetaType.All || Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-light" : "text-muted"; + + + + + } +
+ + @if (!ViewBag.Authorized && !ViewBag.EnablePrivilegedUserPrivacy || ViewBag.Authorized) + { +
+
+ @await Component.InvokeAsync("ProfileMetaList", new { clientId = Model.ClientId, count = 30, offset = 0, startAt = DateTime.UtcNow, metaType = Model.MetaFilterType }) +
+
+ } + +
+ +
+ +
+
-@if ((!ViewBag.Authorized && !ViewBag.EnablePrivilegedUserPrivacy) || ViewBag.Authorized) -{ -
-
- @await Component.InvokeAsync("ProfileMetaList", new { clientId = Model.ClientId, count = 30, offset = 0, startAt = DateTime.UtcNow, metaType = Model.MetaFilterType }) -
-
-} + +@{ + var menuItems = new SideContextMenuItems + { + MenuTitle = "Actions", + }; + + if (Model.Online) + { + menuItems.Items.Add(new SideContextMenuItem + { + Title = "Join Game", + IsLink = true, + IsButton = true, + Reference = Model.ConnectProtocolUrl, + Tooltip = $"Playing on {Model.CurrentServerName.StripColors()}", + Icon = "oi-play-circle" + }); + } + + if (Model.LevelInt != -1 && ViewBag.Authorized) + { + menuItems.Items.Add(new SideContextMenuItem + { + Title = "Change Level", + IsButton = true, + Reference = "edit", + Icon = "oi-cog", + }); + } + + menuItems.Items.Add(new SideContextMenuItem + { + Title = "View Stats", + IsButton = true, + IsLink = true, + Reference = Url.Action("Advanced", "ClientStatistics", new { id = Model.ClientId }), + Icon = "oi-graph", + }); + + if (!isPermBanned && ViewBag.Authorized) + { + menuItems.Items.Add(new SideContextMenuItem + { + Title = isFlagged ? "Unflag" : "Flag", + IsButton = true, + Reference = isFlagged ? "unflag" : "flag", + Icon = "oi-flag" + }); + } + + if ((Model.LevelInt < (int)ViewBag.User.Level && !Model.HasActivePenalty || isTempBanned) && ViewBag.Authorized) + { + menuItems.Items.Add(new SideContextMenuItem + { + Title = "Ban", + IsButton = true, + Reference = "ban", + Icon = "oi-lock-unlocked", + }); + } + + if ((Model.LevelInt < (int)ViewBag.User.Level && Model.HasActivePenalty || isTempBanned) && ViewBag.Authorized) + { + menuItems.Items.Add(new SideContextMenuItem + { + Title = "Unban", + IsButton = true, + Reference = "unban", + Icon = "oi-lock-locked", + }); + } +} + -
-
@section targetid { diff --git a/WebfrontCore/Views/Client/Profile/Meta/_AdministeredPenaltyResponse.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_AdministeredPenaltyResponse.cshtml index a3f592d6d..00251b309 100644 --- a/WebfrontCore/Views/Client/Profile/Meta/_AdministeredPenaltyResponse.cshtml +++ b/WebfrontCore/Views/Client/Profile/Meta/_AdministeredPenaltyResponse.cshtml @@ -1,53 +1,60 @@ @using SharedLibraryCore.Dtos.Meta.Responses @model AdministeredPenaltyResponse @{ - string localizationKey = $"WEBFRONT_CLIENT_META_PENALIZED_{Model.PenaltyType.ToString().ToUpper()}_V2"; + var localizationKey = $"WEBFRONT_CLIENT_META_PENALIZED_{Model.PenaltyType.ToString().ToUpper()}_V2"; } -
- @foreach (var match in Utilities.SplitTranslationTokens(localizationKey)) + + @if (TempData["ShowMetaHeader"] as bool? ?? false) { - if (match.IsInterpolation) - { - if (match.MatchValue == "action") - { - @match.TranslationValue - } - - else if (match.MatchValue == "offender") - { - - - - - - } - - else if (match.MatchValue == "reason") - { - - @if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning) - { - @Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense) - - } - else - { - - } - - - } - - else if (match.MatchValue == "time") - { - @Model.LengthText - } - } - - else - { - @match.MatchValue - } + } -
+ +
+ @foreach (var match in Utilities.SplitTranslationTokens(localizationKey)) + { + if (match.IsInterpolation) + { + if (match.MatchValue == "action") + { + @match.TranslationValue + } + + else if (match.MatchValue == "offender") + { + + + + + + } + + else if (match.MatchValue == "reason") + { + + @if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning) + { + @Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense) + + } + else + { + + } + + + } + + else if (match.MatchValue == "time") + { + @Model.LengthText + } + } + + else + { + @match.MatchValue + } + } +
+ diff --git a/WebfrontCore/Views/Client/Profile/Meta/_ConnectionHistoryResponse.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_ConnectionHistoryResponse.cshtml index c8bb5d3bf..302b71619 100644 --- a/WebfrontCore/Views/Client/Profile/Meta/_ConnectionHistoryResponse.cshtml +++ b/WebfrontCore/Views/Client/Profile/Meta/_ConnectionHistoryResponse.cshtml @@ -4,6 +4,11 @@ var localizationKey = $"WEBFRONT_CLIENT_META_CONNECTION_{Model.ConnectionType.ToString().ToUpper()}"; } +@if (TempData["ShowMetaHeader"] as bool? ?? false) +{ + +} + @foreach (var token in Utilities.SplitTranslationTokens(localizationKey)) { if (token.IsInterpolation) @@ -11,10 +16,10 @@ switch (token.MatchValue) { case "action": - @token.TranslationValue + @token.TranslationValue break; case "server": - + break; diff --git a/WebfrontCore/Views/Client/Profile/Meta/_Information.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_Information.cshtml index 73db2ef79..450ec344d 100644 --- a/WebfrontCore/Views/Client/Profile/Meta/_Information.cshtml +++ b/WebfrontCore/Views/Client/Profile/Meta/_Information.cshtml @@ -1,20 +1,20 @@ @model IEnumerable @{ var informationMeta = Model - .Where(_meta => _meta.Type == SharedLibraryCore.Interfaces.MetaType.Information) - .OrderBy(_meta => _meta.Order) - .GroupBy(_meta => _meta.Column) - .OrderBy(_grouping => _grouping.Key); + .Where(meta => meta.Type == SharedLibraryCore.Interfaces.MetaType.Information) + .OrderBy(meta => meta.Order) + .Select((meta, i) => new { index = i, meta }) + .GroupBy(meta => meta.index / 5); } @foreach (var metaColumn in informationMeta) { -
+
@foreach (var meta in metaColumn) { -
+
- @{var results = Utilities.SplitTranslationTokens(meta.Key);} + @{var results = Utilities.SplitTranslationTokens(meta.meta.Key);} @if (results.Any(_result => _result.IsInterpolation)) { @@ -22,7 +22,7 @@ { if (result.IsInterpolation) { - + } else @@ -34,8 +34,8 @@ else { - - @meta.Key + + @meta.meta.Key }
} diff --git a/WebfrontCore/Views/Client/Profile/Meta/_MessageResponse.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_MessageResponse.cshtml index dd71a207a..57e056318 100644 --- a/WebfrontCore/Views/Client/Profile/Meta/_MessageResponse.cshtml +++ b/WebfrontCore/Views/Client/Profile/Meta/_MessageResponse.cshtml @@ -1,17 +1,24 @@ @using SharedLibraryCore.Dtos.Meta.Responses @model MessageResponse +@if (TempData["ShowMetaHeader"] as bool? ?? false) +{ + +} + - + + + @if (!Model.SentIngame) { [@ViewBag.Localization["WEBFRONT_PROFILE_MESSAGE_EXTERNAL"]] } - + @if (Model.IsHidden && !ViewBag.Authorized) { - + } else diff --git a/WebfrontCore/Views/Client/Profile/Meta/_MetaHeader.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_MetaHeader.cshtml new file mode 100644 index 000000000..b48868969 --- /dev/null +++ b/WebfrontCore/Views/Client/Profile/Meta/_MetaHeader.cshtml @@ -0,0 +1,6 @@ +@model DateTime +@{ Layout = null;} +
+ @Model.HumanizeForCurrentCulture() +
+
diff --git a/WebfrontCore/Views/Client/Profile/Meta/_PermissionLevelChangedResponse.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_PermissionLevelChangedResponse.cshtml index f2c191f44..49f3bcb16 100644 --- a/WebfrontCore/Views/Client/Profile/Meta/_PermissionLevelChangedResponse.cshtml +++ b/WebfrontCore/Views/Client/Profile/Meta/_PermissionLevelChangedResponse.cshtml @@ -1,29 +1,35 @@ @model SharedLibraryCore.Dtos.Meta.Responses.PermissionLevelChangedResponse -@foreach (var token in Utilities.SplitTranslationTokens("WEBFRONT_CLIENT_META_PERMISSION_CHANGED")) -{ - if (token.IsInterpolation) + + @if (TempData["ShowMetaHeader"] as bool? ?? false) { - switch (token.MatchValue) + + } + @foreach (var token in Utilities.SplitTranslationTokens("WEBFRONT_CLIENT_META_PERMISSION_CHANGED")) + { + if (token.IsInterpolation) { - case "permission": - @Model.CurrentPermissionLevel.ToLocalizedLevelName() - break; - case "originClient": - - - - - - break; - case "type": - @token.TranslationValue - break; + switch (token.MatchValue) + { + case "permission": + @Model.CurrentPermissionLevel.ToLocalizedLevelName() + break; + case "originClient": + + + + + + break; + case "type": + @token.TranslationValue + break; + } + } + + else + { + @token.MatchValue } } - - else - { - @token.MatchValue - } -} + diff --git a/WebfrontCore/Views/Client/Profile/Meta/_ReceivedPenaltyResponse.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_ReceivedPenaltyResponse.cshtml index 23c4d8313..44a7c2699 100644 --- a/WebfrontCore/Views/Client/Profile/Meta/_ReceivedPenaltyResponse.cshtml +++ b/WebfrontCore/Views/Client/Profile/Meta/_ReceivedPenaltyResponse.cshtml @@ -2,72 +2,79 @@ @model ReceivedPenaltyResponse @{ - string localizationKey = $"WEBFRONT_CLIENT_META_WAS_PENALIZED_{Model.PenaltyType.ToString().ToUpper()}_V2"; + var localizationKey = $"WEBFRONT_CLIENT_META_WAS_PENALIZED_{Model.PenaltyType.ToString().ToUpper()}_V2"; } -
- @foreach (var match in Utilities.SplitTranslationTokens(localizationKey)) + + @if (TempData["ShowMetaHeader"] as bool? ?? false) { - if (match.IsInterpolation) - { - if (match.MatchValue == "action") - { - @match.TranslationValue - } - - else if (match.MatchValue == "punisher") - { - - - - - - } - - else if (match.MatchValue == "reason") - { - - @if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Kick) - { - @Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense) - - } - else - { - - } - - - } - - else if (match.MatchValue == "time") - { - @Model.LengthText - } - } - - else - { - @match.MatchValue - } + } - @if (Model.ClientId != Model.OffenderClientId) - { - - @foreach (var helperResult in Utilities.SplitTranslationTokens("WEBFRONT_PROFILE_LINKED_ACCOUNT")) +
+ @foreach (var match in Utilities.SplitTranslationTokens(localizationKey)) { - if (!helperResult.IsInterpolation) + if (match.IsInterpolation) { - @helperResult.MatchValue + if (match.MatchValue == "action") + { + @match.TranslationValue + } + + else if (match.MatchValue == "punisher") + { + + + + + + } + + else if (match.MatchValue == "reason") + { + + @if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Kick) + { + @Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense) + + } + else + { + + } + + + } + + else if (match.MatchValue == "time") + { + @Model.LengthText + } } else { - - - + @match.MatchValue } } - } -
+ + @if (Model.ClientId != Model.OffenderClientId) + { + + @foreach (var helperResult in Utilities.SplitTranslationTokens("WEBFRONT_PROFILE_LINKED_ACCOUNT")) + { + if (!helperResult.IsInterpolation) + { + @helperResult.MatchValue + } + + else + { + + + + } + } + } +
+ diff --git a/WebfrontCore/Views/Client/Profile/Meta/_UpdatedAliasResponse.cshtml b/WebfrontCore/Views/Client/Profile/Meta/_UpdatedAliasResponse.cshtml index 3eb7f4a76..b20ab43d0 100644 --- a/WebfrontCore/Views/Client/Profile/Meta/_UpdatedAliasResponse.cshtml +++ b/WebfrontCore/Views/Client/Profile/Meta/_UpdatedAliasResponse.cshtml @@ -1,6 +1,11 @@ @using SharedLibraryCore.Dtos.Meta.Responses @model UpdatedAliasResponse +@if (TempData["ShowMetaHeader"] as bool? ?? false) +{ + +} + @foreach (var token in Utilities.SplitTranslationTokens("WEBFRONT_PROFILE_META_CONNECT_ALIAS")) { if (token.IsInterpolation) @@ -8,12 +13,14 @@ switch (token.MatchValue) { case "action": - @token.TranslationValue + @token.TranslationValue break; case "alias": - + - [@Model.IPAddress] + + [@Model.IPAddress] + break; } diff --git a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml index 7114804a9..8999dc540 100644 --- a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml @@ -6,26 +6,21 @@ @using Humanizer @using Humanizer.Localisation @using IW4MAdmin.Plugins.Stats +@using WebfrontCore.ViewModels @model Stats.Dtos.AdvancedStatsInfo + @{ ViewBag.Title = "Advanced Client Statistics"; ViewBag.Description = Model.ClientName.StripColors(); - const int maxItems = 5; const string headshotKey = "MOD_HEAD_SHOT"; const string headshotKey2 = "headshot"; const string meleeKey = "MOD_MELEE"; - var suicideKeys = new[] {"MOD_SUICIDE", "MOD_FALLING"}; + var suicideKeys = new[] { "MOD_SUICIDE", "MOD_FALLING" }; // if they've not copied default settings config this could be null - var config = (GameStringConfiguration) ViewBag.Config ?? new GameStringConfiguration(); + var config = (GameStringConfiguration)ViewBag.Config ?? new GameStringConfiguration(); - var headerClass = Model.Level == EFClient.Permission.Banned ? "bg-danger" : "bg-primary"; - var textClass = Model.Level == EFClient.Permission.Banned ? "text-danger" : "text-primary"; - var borderBottomClass = Model.Level == EFClient.Permission.Banned ? "border-bottom-danger border-top-danger" : "border-bottom border-top"; - var borderClass = Model.Level == EFClient.Permission.Banned ? "border-danger" : "border-primary"; - var buttonClass = Model.Level == EFClient.Permission.Banned ? "btn-danger" : "btn-primary"; - string GetWeaponNameForHit(EFClientHitStatistic stat) { if (stat == null) @@ -46,7 +41,7 @@ return null; } - var attachmentText = string.Join('+', new[] + var attachmentText = string.Join(" + ", new[] { config.GetStringForGame(attachment.Attachment1.Name, attachment.Attachment1.Game), config.GetStringForGame(attachment.Attachment2?.Name, attachment.Attachment2?.Game), @@ -58,11 +53,11 @@ var weapons = Model.ByWeapon .Where(hit => hit.DamageInflicted > 0 || (hit.DamageInflicted == 0 && hit.HitCount > 0)) - .GroupBy(hit => new {hit.WeaponId}) + .GroupBy(hit => new { hit.WeaponId }) .Select(group => { var withoutAttachments = group.FirstOrDefault(hit => hit.WeaponAttachmentComboId == null); - var mostUsedAttachment = group.Except(new[] {withoutAttachments}) + var mostUsedAttachment = group.Except(new[] { withoutAttachments }) .OrderByDescending(g => g.DamageInflicted) .GroupBy(g => g.WeaponAttachmentComboId) .FirstOrDefault() @@ -72,7 +67,7 @@ { return withoutAttachments; } - + withoutAttachments.WeaponAttachmentComboId = mostUsedAttachment.WeaponAttachmentComboId; withoutAttachments.WeaponAttachmentCombo = mostUsedAttachment.WeaponAttachmentCombo; @@ -107,15 +102,15 @@ .Where(weapon => weapon.DamageInflicted > 0) .GroupBy(weapon => weapon.WeaponId) .Count() - : (int?) null; // want to default to -- in ui instead of 0 + : (int?)null; // want to default to -- in ui instead of 0 var activeTime = weapons.Any() ? TimeSpan.FromSeconds(weapons.Sum(weapon => weapon.UsageSeconds ?? 0)) - : (TimeSpan?) null; // want to default to -- in ui instead of 0 + : (TimeSpan?)null; // want to default to -- in ui instead of 0 var kdr = aggregate == null ? null - : Math.Round(aggregate.KillCount / (float) aggregate.DeathCount, 2).ToString(Utilities.CurrentLocalization.Culture); + : Math.Round(aggregate.KillCount / (float)aggregate.DeathCount, 2).ToString(Utilities.CurrentLocalization.Culture); var serverLegacyStat = Model.LegacyStats .FirstOrDefault(stat => stat.ServerId == Model.ServerId); @@ -140,15 +135,15 @@ var headShots = allPerServer.Any() ? allPerServer.Where(hit => hit.MeansOfDeath?.Name == headshotKey || hit.HitLocation?.Name == headshotKey2).Sum(hit => hit.HitCount) - : (int?) null; // want to default to -- in ui instead of 0 + : (int?)null; // want to default to -- in ui instead of 0 var meleeKills = allPerServer.Any() ? allPerServer.Where(hit => hit.MeansOfDeath?.Name == meleeKey).Sum(hit => hit.KillCount) - : (int?) null; + : (int?)null; var suicides = allPerServer.Any() ? allPerServer.Where(hit => suicideKeys.Contains(hit.MeansOfDeath?.Name ?? "")).Sum(hit => hit.KillCount) - : (int?) null; + : (int?)null; var statCards = new[] { @@ -172,7 +167,7 @@ Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"] as string).Titleize(), Value = score.ToNumericalString() }, - new + new { Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"] as string), Value = Model.ZScore.ToNumericalString(2) @@ -235,205 +230,170 @@ }; } -
- -
-
+
+ +
+

Player Stats

+ + + -
-
- @performance -
-
-
- @Model.ClientName.StripColors() + +
+
+ + @performance + +
+ + @if (Model.Level == EFClient.Permission.Banned) + { +
@ViewBag.Localization["GLOBAL_PERMISSION_BANNED"]
+ } + else if (Model.ZScore != null) + { + if (Model.Ranking > 0) + { +
@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED"] as string).FormatExt(Model.Ranking))
+ } + + else + { +
@ViewBag.Localization["WEBFRONT_ADV_STATS_EXPIRED"]
+ } + if (Model.ServerId != null) + { +
@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_PERFORMANCE"] as string).FormatExt($"{performance.ToNumericalString()}"))
+ } + + else + { +
@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RATING"] as string).FormatExt($"{Model.Rating.ToNumericalString()}"))
+ } + } + + else + { +
@ViewBag.Localization["WEBFRONT_STATS_INDEX_UNRANKED"]
+ } +
+ + @if (performanceHistory.Count() > 5) + { +
+ +
+ }
- @if (Model.Level == EFClient.Permission.Banned) - { -
@ViewBag.Localization["GLOBAL_PERMISSION_BANNED"]
- } - else if (Model.ZScore != null) - { - if (Model.ServerId != null) +
+
+ @foreach (var card in statCards) { -
@((ViewBag.Localization["WEBFRONT_ADV_STATS_PERFORMANCE"] as string).FormatExt(performance.ToNumericalString()))
+
+ @if (string.IsNullOrWhiteSpace(card.Value)) + { +
+ } + else + { +
@card.Value
+ } +
@card.Name
+
} - - else - { -
@((ViewBag.Localization["WEBFRONT_ADV_STATS_RATING"] as string).FormatExt(Model.Rating.ToNumericalString()))
- } - - if (Model.Ranking > 0) - { -
@((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED"] as string).FormatExt(Model.Ranking.ToNumericalString()))
- } - - else - { -
@ViewBag.Localization["WEBFRONT_ADV_STATS_EXPIRED"]
- } - } - - else - { -
@ViewBag.Localization["WEBFRONT_STATS_INDEX_UNRANKED"]
- } +
-
-
- -
-
- -
- @foreach (var card in statCards) - { -
- @if (string.IsNullOrWhiteSpace(card.Value)) - { -
- } - else - { -
@card.Value
- } -
@card.Name
-
- } -
-
- -
-
-
@ViewBag.Localization["WEBFRONT_ADV_STATS_WEAP_USAGE"]
-
- - - - - - - - - - @foreach (var weaponHit in weapons.Take(maxItems)) - { - - - @{ var attachments = GetWeaponAttachmentName(weaponHit.WeaponAttachmentCombo); } - @if (string.IsNullOrWhiteSpace(attachments)) - { - - } - else - { - - } - - - - - - } - - @foreach (var weaponHit in weapons.Skip(maxItems)) - { - - - @{ var attachments = GetWeaponAttachmentName(weaponHit.WeaponAttachmentCombo); } - @if (string.IsNullOrWhiteSpace(attachments)) - { - - } - else - { - - } - - - - - - } - -
@ViewBag.Localization["WEBFRONT_ADV_STATS_WEAPON"]@ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_ATTACHMENTS"]@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]@ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"]@ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"]@ViewBag.Localization["WEBFRONT_ADV_STATS_USAGE"]
@GetWeaponNameForHit(weaponHit)@attachments@weaponHit.KillCount.ToNumericalString()@weaponHit.HitCount.ToNumericalString()@weaponHit.DamageInflicted.ToNumericalString()@TimeSpan.FromSeconds(weaponHit.UsageSeconds ?? 0).HumanizeForCurrentCulture(minUnit: TimeUnit.Second)
- -
-
-
- -
-
-
@ViewBag.Localization["WEBFRONT_ADV_STATS_HIT_LOCATIONS"]
-
- - - - - - - +
+ @{ var totalHits = filteredHitLocations.Sum(hit => hit.HitCount); - } - @foreach (var hitLocation in filteredHitLocations.Take(8)) - { -
- - - - - + + var hitLocationsTable = new TableInfo(5) + { + Header = ViewBag.Localization["WEBFRONT_ADV_STATS_HIT_LOCATIONS"] + }; + + hitLocationsTable.WithColumns(new string[] + { + ViewBag.Localization["WEBFRONT_ADV_STATS_LOCATION"], + ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"], + ViewBag.Localization["WEBFRONT_ADV_STATS_PERCENTAGE"], + ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"], + }).WithRows(filteredHitLocations, hitLocation => new[] + { + config.GetStringForGame(hitLocation.HitLocation.Name, hitLocation.HitLocation.Game), + hitLocation.HitCount.ToString(), + $"{Math.Round((hitLocation.HitCount / (float)totalHits) * 100.0).ToString(Utilities.CurrentLocalization.Culture)}%", + hitLocation.DamageInflicted.ToNumericalString() + }); } - @foreach (var hitLocation in filteredHitLocations.Skip(8)) - { - - - - - - +
+ + +
+ + +
+
+ + + @{ + var weaponsUsedTable = new TableInfo(10) + { + Header = ViewBag.Localization["WEBFRONT_ADV_STATS_WEAP_USAGE"] + }; + + weaponsUsedTable.WithColumns(new string[] + { + ViewBag.Localization["WEBFRONT_ADV_STATS_WEAPON"], + ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_ATTACHMENTS"], + ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"], + ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"], + ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"], + ViewBag.Localization["WEBFRONT_ADV_STATS_USAGE"] + }).WithRows(weapons, weapon => new[] + { + GetWeaponNameForHit(weapon), + GetWeaponAttachmentName(weapon.WeaponAttachmentCombo) ?? "--", + weapon.KillCount.ToNumericalString(), + weapon.HitCount.ToNumericalString(), + weapon.DamageInflicted.ToNumericalString(), + TimeSpan.FromSeconds(weapon.UsageSeconds ?? 0).HumanizeForCurrentCulture(minUnit: TimeUnit.Second) + }); } -
@ViewBag.Localization["WEBFRONT_ADV_STATS_LOCATION"]@ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"]@ViewBag.Localization["WEBFRONT_ADV_STATS_PERCENTAGE"]@ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"]
@config.GetStringForGame(hitLocation.HitLocation.Name, hitLocation.HitLocation.Game)@hitLocation.HitCount@Math.Round((hitLocation.HitCount / (float) totalHits) * 100.0).ToString(Utilities.CurrentLocalization.Culture)%@hitLocation.DamageInflicted.ToNumericalString()
- -
-
-
- - + +
+ +
+ + @{ + var menuItems = new SideContextMenuItems + { + MenuTitle = "Game", Items = Model.Servers.Select(server => new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("Advanced", "ClientStatistics", new { serverId = server.Endpoint }), + Title = server.Name.StripColors(), + IsActive = Model.ServerEndpoint == server.Endpoint + }).Prepend(new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("Advanced", "ClientStatistics"), + Title = ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"], + IsActive = Model.ServerEndpoint is null + }).ToList() + }; + } +
+ @{ var projection = filteredHitLocations.Select(loc => new { @@ -441,7 +401,7 @@ // we want to count head and neck as the same percentage = (loc.HitLocation.Name == "head" ? filteredHitLocations.FirstOrDefault(c => c.HitLocation.Name == "neck")?.HitCount ?? 0 + loc.HitCount - : loc.HitCount) / (float) totalHits + : loc.HitCount) / (float)totalHits }).ToList(); var maxPercentage = projection.Any() ? projection.Max(p => p.percentage) : 0; } @@ -456,4 +416,4 @@ -} \ No newline at end of file +} diff --git a/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml b/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml index b53759e44..efadf8a53 100644 --- a/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml @@ -3,149 +3,90 @@ @{ Layout = null; var loc = Utilities.CurrentLocalization.LocalizationIndex.Set; - double getDeviation(double deviations) => Math.Pow(Math.E, 5.259 + (deviations * 0.812)); - string rankIcon(double? elo) - { - if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25)) - { - return "0_no-place/menu_div_no_place.png"; - } - if (elo >= getDeviation(0.125) && elo < getDeviation(0.625)) - { - return "1_iron/menu_div_iron_sub03.png"; - } - if (elo >= getDeviation(0.625) && elo < getDeviation(1.0)) - { - return "2_bronze/menu_div_bronze_sub03.png"; - } - if (elo >= getDeviation(1.0) && elo < getDeviation(1.25)) - { - return "3_silver/menu_div_silver_sub03.png"; - } - if (elo >= getDeviation(1.25) && elo < getDeviation(1.5)) - { - return "4_gold/menu_div_gold_sub03.png"; - } - if (elo >= getDeviation(1.5) && elo < getDeviation(1.75)) - { - return "5_platinum/menu_div_platinum_sub03.png"; - } - if (elo >= getDeviation(1.75) && elo < getDeviation(2.0)) - { - return "6_semipro/menu_div_semipro_sub03.png"; - } - if (elo >= getDeviation(2.0)) - { - return "7_pro/menu_div_pro_sub03.png"; - } - - return "0_no-place/menu_div_no_place.png"; - } } @if (Model.Count == 0) { -
@Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_NOQUALIFY"]
+
+
+ +
+

@Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_NOQUALIFY"]

+ Check back after some more time has passed +
+
+
} + @foreach (var stat in Model) { -
- @if (ViewBag.UseNewStats) - { - @stat.Performance - } -
-
-
#@stat.Ranking
- @if (stat.RatingChange > 0) - { -
-
-
@stat.RatingChange
+
+
+
+
+ # +
@stat.Ranking
+
+
+
+ + + + @if (stat.RatingChange > 0) + { +
+
+
@stat.RatingChange
+
+ } + @if (stat.RatingChange < 0) + { +
+
@Math.Abs(stat.RatingChange)
+
+
+ }
- } - @if (stat.RatingChange < 0) - { -
-
@Math.Abs(stat.RatingChange)
-
-
- } - - @if (!ViewBag.UseNewStats) - { - - - - } - else - { - - - - } -
- @if (ViewBag.UseNewStats) - { -
-
- +
+ @stat.Performance.ToNumericalString() @if (stat.ServerId == null) { - @loc["WEBFRONT_ADV_STATS_RATING"].FormatExt("").ToLower() + @loc["WEBFRONT_ADV_STATS_RATING"].FormatExt("").ToLower() } else { - @loc["WEBFRONT_ADV_STATS_PERFORMANCE"].FormatExt("").ToLower() + @loc["WEBFRONT_ADV_STATS_PERFORMANCE"].FormatExt("").ToLower() }
-
- @stat.Kills.ToNumericalString() @loc["PLUGINS_STATS_TEXT_KILLS"] -
-
- @stat.Deaths.ToNumericalString() @loc["PLUGINS_STATS_TEXT_DEATHS"]
-
-
- @stat.KDR @loc["PLUGINS_STATS_TEXT_KDR"] -
-
- @stat.TimePlayedValue.HumanizeForCurrentCulture() @loc["WEBFRONT_PROFILE_PLAYER"] -
-
- @stat.LastSeenValue.HumanizeForCurrentCulture() @loc["WEBFRONT_PROFILE_LSEEN"] -
- } - else - { - @stat.Performance @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"] -
- @stat.KDR @loc["PLUGINS_STATS_TEXT_KDR"] - @stat.Kills @loc["PLUGINS_STATS_TEXT_KILLS"] - @stat.Deaths @loc["PLUGINS_STATS_TEXT_DEATHS"]
- @loc["WEBFRONT_PROFILE_PLAYER"] @stat.TimePlayed @loc["GLOBAL_TIME_HOURS"]
- @loc["WEBFRONT_PROFILE_LSEEN"] @stat.LastSeen @loc["WEBFRONT_PENALTY_TEMPLATE_AGO"] - } +
+ +
+
+ @stat.Kills.ToNumericalString() @loc["PLUGINS_STATS_TEXT_KILLS"] +
+
+ @stat.Deaths.ToNumericalString() @loc["PLUGINS_STATS_TEXT_DEATHS"]
+
+
+ @stat.KDR @loc["PLUGINS_STATS_TEXT_KDR"] +
+
+ @stat.TimePlayedValue.HumanizeForCurrentCulture() @loc["WEBFRONT_PROFILE_PLAYER"] +
+
+ @stat.LastSeenValue.HumanizeForCurrentCulture() @loc["WEBFRONT_PROFILE_LSEEN"] +
+
- -
- +
- -
- @if (ViewBag.UseNewStats) - { - @stat.Performance - } - - else - { - - } +
+ @stat.Performance
} diff --git a/WebfrontCore/Views/Client/Statistics/Index.cshtml b/WebfrontCore/Views/Client/Statistics/Index.cshtml index 73b2c47d9..473b61961 100644 --- a/WebfrontCore/Views/Client/Statistics/Index.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Index.cshtml @@ -1,34 +1,48 @@ - -
-
- @await Component.InvokeAsync("TopPlayers", new { count = 25, offset = 0 }) +
+
+

Top Players

+ + + + +
+ @await Component.InvokeAsync("TopPlayers", new { count = 25, offset = 0, serverEndpoint = ViewBag.SelectedServerId }) +
+
+ +
- @foreach (var server in ViewBag.Servers) - { -
-
+ + @{ + var menuItems = new SideContextMenuItems + { + MenuTitle = "Game", Items = Model.Select(server => new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("TopPlayers", "Stats", new { serverId = server.Endpoint }), + Title = server.Name.StripColors(), + IsActive = ViewBag.SelectedServerId == server.Endpoint + }).Prepend(new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("TopPlayers", "Stats"), + Title = ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"], + IsActive = ViewBag.SelectedServerId is null + }).ToList() + }; } +
@section scripts - { +{ - + } diff --git a/WebfrontCore/Views/Client/_MessageContext.cshtml b/WebfrontCore/Views/Client/_MessageContext.cshtml index c9adc1910..3abdf2693 100644 --- a/WebfrontCore/Views/Client/_MessageContext.cshtml +++ b/WebfrontCore/Views/Client/_MessageContext.cshtml @@ -1,24 +1,25 @@ @using SharedLibraryCore.Dtos.Meta.Responses +@using Humanizer @model IList @{ Layout = null; } -
-
@Model.First().When.ToString()
-
+
+
@Model.First().When.Humanize()
+
@foreach (var message in Model) { - - + + — - - + +
}
-
\ No newline at end of file +
diff --git a/WebfrontCore/Views/Client/_PenaltyInfo.cshtml b/WebfrontCore/Views/Client/_PenaltyInfo.cshtml index 667cd059b..fcead07a0 100644 --- a/WebfrontCore/Views/Client/_PenaltyInfo.cshtml +++ b/WebfrontCore/Views/Client/_PenaltyInfo.cshtml @@ -3,7 +3,7 @@ Layout = null; } -
+
@foreach (var snapshot in Model) { @@ -15,9 +15,9 @@ continue; } - @prop.Name + @prop.Name — @prop.GetValue(snapshot)?.ToString()?.StripColors()
} -
+
} -
\ No newline at end of file +
diff --git a/WebfrontCore/Views/Configuration/Files.cshtml b/WebfrontCore/Views/Configuration/Files.cshtml index e7757bea0..4ae1402e7 100644 --- a/WebfrontCore/Views/Configuration/Files.cshtml +++ b/WebfrontCore/Views/Configuration/Files.cshtml @@ -12,53 +12,36 @@ } -
-
-

@ViewData["Title"]

-
@noticeText
+
+

@ViewData["Title"]

+ @noticeText - - -
-
+ @foreach (var file in Model.OrderByDescending(f => f.FileName.Contains("IW4MAdmin"))) + { +
+
+ + @file.FileName
-
- @foreach (var file in Model) - { -
- - @file.FileName -
-
-
@file.FileContent
- -
- } +
+
@file.FileContent
+
+ } +
-
- - @section scripts - { - - - + - - - - } - -
\ No newline at end of file + + + +} diff --git a/WebfrontCore/Views/Configuration/Index.cshtml b/WebfrontCore/Views/Configuration/Index.cshtml index 98b5fed2a..112deccb8 100644 --- a/WebfrontCore/Views/Configuration/Index.cshtml +++ b/WebfrontCore/Views/Configuration/Index.cshtml @@ -140,4 +140,4 @@ -} \ No newline at end of file +} diff --git a/WebfrontCore/Views/Console/Index.cshtml b/WebfrontCore/Views/Console/Index.cshtml index a1281199a..6c7ce5169 100644 --- a/WebfrontCore/Views/Console/Index.cshtml +++ b/WebfrontCore/Views/Console/Index.cshtml @@ -1,16 +1,54 @@ @model IEnumerable -
-
- @Html.DropDownList("Server", Model.Select(s => new SelectListItem() { Text = SharedLibraryCore.Utilities.StripColors(s.Name), Value = s.ID.ToString() }).ToList(), new { @class = "form-control bg-dark text-light", id = "console_server_select" }) -
+
+

Web Console

+ +
+
+
                                /(((((((*                           
+                      ./((((((((((((((((,                            
+            .*/(((((((((((((((((((((((((.                            
+    *((((((((((((((((((((((((((((((((((                              
+ ./(((((((((((((((((((((((((((((((((((/ ,**,                         
+    ./(((((((((((((((((((((((((((((((/  ,******,                     
+       ,(((((((((((((((((((((((((((((* ********//**.                 
+         */(((((((((((((((((((((((((* ***************                
+      ,***  ,(((((((((((((((((((((((,  ,***************.             
+     ,****/**,*/(((((((((((((((((((.       *************,            
+    ************,,((((((((((((((((*.         ,************           
+  .***********,     *(((((((((((((            .*//*********          
+ .,**********,        ,(((((((((/               ***********          
+ ,**********,           *(((((((/               ,**********          
+ ,/*********,             .*(((*                 **********,         
+ ,/*********,                .*.                 ***********.        
+ ,**********,                                   ***********          
+  .***********                                  ***********          
+  .************                               .***********           
+    ************.                           .************,           
+     ,*************                      .**************,            
+      .*****************              *****************.             
+         *******************************************,                
+           .*//***********************************.                  
+              ********************************,.                     
+                 .************************,                          
+                            ,.**,           
+            
-
-
- +
+ +
+
+
+ Server
-
- + @Html.DropDownList("Server", Model.Select(s => new SelectListItem { Text = s.Name.StripColors(), Value = s.ID.ToString() }).ToList(), new { @class = "form-control", id = "console_server_select" }) +
+
+ +
+
diff --git a/WebfrontCore/Views/Home/Help.cshtml b/WebfrontCore/Views/Home/Help.cshtml index 0d91dd4dc..909331266 100644 --- a/WebfrontCore/Views/Home/Help.cshtml +++ b/WebfrontCore/Views/Home/Help.cshtml @@ -1,74 +1,58 @@ @model IEnumerable<(string, IEnumerable)> @{ - var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; + var loc = Utilities.CurrentLocalization.LocalizationIndex; } -@foreach ((var pluginName, var commandList) in Model) -{ -

@pluginName

- - - - - - - - - - - +
+ @foreach (var (pluginName, commandList) in Model) + { +

@(pluginName == "Native" ? "Command List" : pluginName)

+ +
@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]
+ + + + + + + + + - - + + @foreach (var command in commandList) { - - + + + - - + + + + + + + + } - -
@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]
@command.Name
@command.Name @command.Alias @command.Description @command.RequiresTarget@ViewBag.CommandPrefix@command.Syntax.Split(@ViewBag.CommandPrefix)[1]@command.Permission.ToLocalizedLevelName()@ViewBag.CommandPrefix@command.Syntax.Split(ViewBag.CommandPrefix)[1]@command.Permission.ToLocalizedLevelName()
+
@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]
+
@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]
+
@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]
+
@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]
+
@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]
+
@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]
+
+
@command.Name
+
@command.Alias
+
@command.Description
+
@command.RequiresTarget
+
@ViewBag.CommandPrefix@command.Syntax.Split(ViewBag.CommandPrefix)[1]
+
@command.Permission.ToLocalizedLevelName()
+
- - - - - - - - - - - @foreach (var command in commandList) - { - - - - - - - - - - - - - - - - - - - - - - - - - } - -
@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]@command.Name
@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]@command.Alias
@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]@command.Description
@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]@command.RequiresTarget
@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]@ViewBag.CommandPrefix@command.Syntax.Split(@ViewBag.CommandPrefix)[1]
@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]@command.Permission.ToLocalizedLevelName()
-} \ No newline at end of file + + + } +
diff --git a/WebfrontCore/Views/Home/Index.cshtml b/WebfrontCore/Views/Home/Index.cshtml index 2ce89dbd6..71ead1e2a 100644 --- a/WebfrontCore/Views/Home/Index.cshtml +++ b/WebfrontCore/Views/Home/Index.cshtml @@ -1,44 +1,57 @@ -@model SharedLibraryCore.Dtos.IW4MAdminInfo -@{ - var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; - string formatTranslation(string translationKey, params object[] values) +@using WebfrontCore.ViewModels +@using Humanizer +@model SharedLibraryCore.Dtos.IW4MAdminInfo +@{ + var loc = Utilities.CurrentLocalization.LocalizationIndex; + + string FormatTranslation(string translationKey, params object[] values) { var split = loc[translationKey].Split("::"); - return split.Count() == 2 ? $"{SharedLibraryCore.Utilities.FormatExt(split[0], values)}{split[1]}" : translationKey; + return split.Length == 2 ? $"{split[0].FormatExt(values)}{split[1]}" : translationKey; } } -
-
-
@Html.Raw(formatTranslation("WEBFRONT_HOME_CLIENTS_ONLINE", Model.TotalOccupiedClientSlots, Model.TotalAvailableClientSlots))
-
-
-
@Html.Raw(formatTranslation("WEBFRONT_HOME_MAX_CONCURRENT_CLIENTS", Model.MaxConcurrentClients.ToString("#,##0")))
-
-
-
@Html.Raw(formatTranslation("WEBFRONT_HOME_RECENT_CLIENTS", Model.RecentClientCount.ToString("#,##0")))
-
-
-
@Html.Raw(formatTranslation("WEBFRONT_HOME_TOTAL_CLIENTS", Model.TotalClientCount.ToString("#,##0")))
-
-
- -@if (Model.ActiveServerGames.Length > 1) -{ - -} - -@await Component.InvokeAsync("ServerList", Model.Game) + else + { + @ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"] + } +
+
+
@Html.Raw(FormatTranslation("WEBFRONT_HOME_CLIENTS_ONLINE", Model.TotalOccupiedClientSlots, Model.TotalAvailableClientSlots))
+
@Html.Raw(FormatTranslation("WEBFRONT_HOME_MAX_CONCURRENT_CLIENTS", Model.MaxConcurrentClients.ToString("#,##0")))
+
@Html.Raw(FormatTranslation("WEBFRONT_HOME_RECENT_CLIENTS", Model.RecentClientCount.ToString("#,##0")))
+
@Html.Raw(FormatTranslation("WEBFRONT_HOME_TOTAL_CLIENTS", Model.TotalClientCount.ToString("#,##0")))
+
+
+ @await Component.InvokeAsync("ServerList", Model.Game) +
+ + @{ + var menuItems = new SideContextMenuItems + { + MenuTitle = "Game", Items = Model.ActiveServerGames.Select(game => new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("Index", "Home", new { game }), + Title = loc[$"GAME_{game}"], + IsActive = game == Model.Game + }).Prepend(new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("Index", "Home"), + Title = loc["GAME_ALL"], + IsActive = !Model.Game.HasValue + }).ToList() + }; + } + +
@section scripts { diff --git a/WebfrontCore/Views/Penalty/List.cshtml b/WebfrontCore/Views/Penalty/List.cshtml index 1df43ff42..7b8787122 100644 --- a/WebfrontCore/Views/Penalty/List.cshtml +++ b/WebfrontCore/Views/Penalty/List.cshtml @@ -1,86 +1,101 @@ -@model Data.Models.EFPenalty.PenaltyType +@using Humanizer +@using Data.Models +@model Data.Models.EFPenalty.PenaltyType @{ - var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; + var loc = Utilities.CurrentLocalization.LocalizationIndex; } -

@ViewBag.Title

-
-
- } else { - + } - } - else - { - if ((Data.Models.EFPenalty.PenaltyType)penaltyType == Model) + +
+
+ + +
+
+ @loc["WEBFRONT_PENALTY_TEMPLATE_SHOWONLY"].Titleize() +
+ -
- -
-
- @loc["WEBFRONT_PENALTY_HIDE_AUTOMATED"] -
-
-
-
- - - - - - - - - + + + + - - @await Component.InvokeAsync("PenaltyList", new WebfrontCore.ViewModels.PenaltyFilterInfo() - { - Offset = 0, - ShowOnly = Model, - IgnoreAutomated = ViewBag.HideAutomatedPenalties - }) + + @await Component.InvokeAsync("PenaltyList", new WebfrontCore.ViewModels.PenaltyFilterInfo + { + Offset = 0, + Count = 30, + ShowOnly = Model, + IgnoreAutomated = ViewBag.HideAutomatedPenalties, + })
@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]@loc["WEBFRONT_PENALTY_TEMPLATE_OFFENSE"]@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]@loc["WEBFRONT_PENALTY_TEMPLATE_TIME"]
- - -
- + +
+ +
+
@section scripts { + + } diff --git a/WebfrontCore/Views/Penalty/_List.cshtml b/WebfrontCore/Views/Penalty/_List.cshtml index 57658559b..cbe885888 100644 --- a/WebfrontCore/Views/Penalty/_List.cshtml +++ b/WebfrontCore/Views/Penalty/_List.cshtml @@ -4,4 +4,4 @@ @model WebfrontCore.ViewModels.PenaltyFilterInfo -@await Component.InvokeAsync("PenaltyList", new { offset = Model.Offset, showOnly = Model.ShowOnly, ignoreAutomated = Model.IgnoreAutomated }) \ No newline at end of file +@await Component.InvokeAsync("PenaltyList", new { offset = Model.Offset, count= Model.Count, showOnly = Model.ShowOnly, ignoreAutomated = Model.IgnoreAutomated }) diff --git a/WebfrontCore/Views/Penalty/_Penalty.cshtml b/WebfrontCore/Views/Penalty/_Penalty.cshtml index 75cda93b5..9cbe7326e 100644 --- a/WebfrontCore/Views/Penalty/_Penalty.cshtml +++ b/WebfrontCore/Views/Penalty/_Penalty.cshtml @@ -1,84 +1,70 @@ @{ Layout = null; - var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; + var loc = Utilities.CurrentLocalization.LocalizationIndex; + + var canSeeLevel = (ViewBag.PermissionsSet as IEnumerable).HasPermission(WebfrontEntity.ClientLevel, WebfrontPermission.Read) && Model.PunisherLevel != 0; + var punisherLevelClass = canSeeLevel ? $"level-color-{(int)Model.PunisherLevel}" : "text-light-dm text-dark-lm"; } +@using WebfrontCore.Permissions @model SharedLibraryCore.Dtos.PenaltyInfo - - @loc["WEBFRONT_PENALTY_TEMPLATE_NAME"] - - - - - - - - - @loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"] - - @Model.PenaltyType - - - - - @loc["WEBFRONT_PENALTY_TEMPLATE_OFFENSE"] - - - - - - - @loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"] - - @Html.ActionLink(SharedLibraryCore.Utilities.StripColors(Model.PunisherName), "ProfileAsync", - "Client", - new { id = Model.PunisherId }, - new { @class = !ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy - ? "level-color-0" - : "level-color-" + (int)Model.PunisherLevel }) - - - - - @loc["WEBFRONT_PENALTY_TEMPLATE_TIME"] - - @{ - if (Model.Expired) - { - @Model.TimePunishedString - } - else - { - @Model.TimeRemaining - } - } - - - + - - + + - + @Model.PenaltyType - + - - @Html.ActionLink(SharedLibraryCore.Utilities.StripColors(Model.PunisherName), "ProfileAsync", - "Client", - new { id = Model.PunisherId }, - new { @class = !ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy - ? "level-color-0" - : "level-color-" + (int)Model.PunisherLevel }) + + + + - - @{ - if (Model.Expired) + + @if (Model.Expired) + { + @Model.TimePunishedString + } + else + { + @Model.TimeRemaining + } + + + + + + +
@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]
+
@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]
+
@loc["WEBFRONT_PENALTY_TEMPLATE_OFFENSE"]
+
@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]
+
@loc["WEBFRONT_PENALTY_TEMPLATE_TIME"]
+ + +
+ + + +
+
+ @Model.PenaltyType +
+
+ +
+ + + +
+ @if (Model.Expired) { @Model.TimePunishedString } @@ -86,6 +72,6 @@ { @Model.TimeRemaining } - } +
diff --git a/WebfrontCore/Views/Server/Scoreboard.cshtml b/WebfrontCore/Views/Server/Scoreboard.cshtml index 6c687699c..7fa0e23c0 100644 --- a/WebfrontCore/Views/Server/Scoreboard.cshtml +++ b/WebfrontCore/Views/Server/Scoreboard.cshtml @@ -1,26 +1,28 @@ -@model IEnumerable +@using WebfrontCore.ViewModels +@model IEnumerable - -
- @{ i = 0; } - @foreach (var server in Model) - { -
- @await Html.PartialAsync("_Scoreboard", server) -
- i++; +
+
+ @if (Model is not null) + { +
+ @await Html.PartialAsync("_Scoreboard", Model.FirstOrDefault(server => server.ServerId == ViewBag.SelectedServerId) ?? Model.First()) +
+ } +
+ @{ + var menuItems = new SideContextMenuItems + { + MenuTitle = "Server", Items = Model.Select(server => new SideContextMenuItem + { + IsLink = true, + Reference = Url.Action("Scoreboard", "Server", new { serverId = server.ServerId }), + Title = server.ServerName.StripColors(), + IsActive = ViewBag.SelectedServerId == server.ServerId + }).ToList() + }; } +
@section scripts { diff --git a/WebfrontCore/Views/Server/_ClientActivity.cshtml b/WebfrontCore/Views/Server/_ClientActivity.cshtml index 791502fde..6d12aa746 100644 --- a/WebfrontCore/Views/Server/_ClientActivity.cshtml +++ b/WebfrontCore/Views/Server/_ClientActivity.cshtml @@ -2,146 +2,81 @@ @{ Layout = null; - int half = Model.ClientCount == 0 || Model.Players.Count == 0 ? 0 : (int)Math.Ceiling(Model.ClientCount / 2.0); -} -
- @{ - for (int i = 0; i < Model.ChatHistory.Count; i++) + var half = Model.ClientCount == 0 || Model.Players.Count == 0 ? 0 : (int)Math.Ceiling(Model.ClientCount / 2.0); + var groupedClients = Model.Players.Select((client, i) => new { index = i, client }) + .OrderBy(client => client.client.Name) + .GroupBy(client => client.index >= half).Select((group, index) => new { - if (Model.ChatHistory[i] == null || - Model.ChatHistory[i].Message == null || - Model.ChatHistory[i].Name == null) - { - continue; - } + group, + index + }).ToList(); - string message = Model.ChatHistory[i].IsHidden && !ViewBag.Authorized ? Model.ChatHistory[i].HiddenMessage : Model.ChatHistory[i].Message; - - if (Model.ChatHistory[i].Message == "CONNECTED") - { - - - -
- } - if (Model.ChatHistory[i].Message == "DISCONNECTED") - { - - - -
- } - if (Model.ChatHistory[i].Message != "CONNECTED" && Model.ChatHistory[i].Message != "DISCONNECTED") - { - - - - - — - -
- } - } + string GetIconForState(string messageState) + { + return messageState switch + { + "CONNECTED" => "oi-account-login text-success mr-5", + "DISCONNECTED" => "oi-account-logout text-danger mr-5", + _ => "" + }; } -
-
-
-
- @{ - for (int i = 0; i < half; i++) - { - if (i > Model.Players.Count - 1) +} + +
+ @if (groupedClients.Count > 0) + { +
+ @foreach (var chat in Model.ChatHistory) + { + var message = chat.IsHidden && !ViewBag.Authorized ? chat.HiddenMessage : chat.Message; + var stateIcon = GetIconForState(chat.Message); + +
+ + + + + + @if (stateIcon == "") { - continue; + + — + + + + } - - string levelColorClass = !ViewBag.Authorized ? "" : $"level-color-{Model.Players[i].LevelInt}"; -
- @if (ViewBag.Authorized) - { - - } - - - - - - @if (ViewBag.Authorized) - { - - } -
-
- } +
+ } +
-
- @{ - for (int i = half; i < Math.Min(Model.ClientCount, Model.Players.Count); i++) + } + +
+ + @foreach (var clientIndex in groupedClients) + { +
+ @foreach (var client in clientIndex.group) { - if (i > Model.Players.Count - 1) - { - continue; - } - - string levelColorClass = !ViewBag.Authorized ? "" : $"level-color-{Model.Players[i].LevelInt}"; - - +
+ } + @if (groupedClients.Count > 0) + { +
+ }
-@if (Model.ChatHistory.Count > 0) -{ -
-} -
- @{ - for (int i = 0; i < Model.ChatHistory.Count; i++) - { - if (Model.ChatHistory[i] == null || - Model.ChatHistory[i].Message == null || - Model.ChatHistory[i].Name == null) - { - continue; - } - - string message = Model.ChatHistory[i].IsHidden && !ViewBag.Authorized ? Model.ChatHistory[i].HiddenMessage : Model.ChatHistory[i].Message; - - if (Model.ChatHistory[i].Message == "CONNECTED") - { - - - -
- } - if (Model.ChatHistory[i].Message == "DISCONNECTED") - { - - - -
- } - if (Model.ChatHistory[i].Message != "CONNECTED" && Model.ChatHistory[i].Message != "DISCONNECTED") - { - - - - - — - -
- } - } - } -
diff --git a/WebfrontCore/Views/Server/_Scoreboard.cshtml b/WebfrontCore/Views/Server/_Scoreboard.cshtml index 3438b9857..d0e097d2e 100644 --- a/WebfrontCore/Views/Server/_Scoreboard.cshtml +++ b/WebfrontCore/Views/Server/_Scoreboard.cshtml @@ -1,5 +1,6 @@ @using WebfrontCore.ViewModels @using System.Globalization +@using SharedLibraryCore.Database.Models @model WebfrontCore.ViewModels.ScoreboardInfo @{ Layout = null; @@ -16,17 +17,29 @@ { if (propertyName == (Model.OrderByKey ?? nameof(ClientScoreboardInfo.Score))) { - return Model.ShouldOrderDescending ? "" : ""; + return Model.ShouldOrderDescending ? "" : ""; } return null; } + + string GetTeamBackgroundColorClass(ClientScoreboardInfo client) + { + return $"team-{client.Team.ToString().ToLower()}-bg {(client.Team == EFClient.TeamType.Unknown ? "bg-dark-dm bg-light-lm" : "")}"; + } } - + Scoreboard + + + + + +
- + @@ -38,9 +51,10 @@ @foreach (var client in Model.ShouldOrderDescending ? Model.ClientInfo.OrderByDescending(OrderByFunc) : Model.ClientInfo.OrderBy(OrderByFunc)) { - + + @@ -52,5 +66,31 @@ + + + + + + }
@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PLAYER"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.ClientName))) @ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Score))) @ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Kills)))
- + @(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture)) @client.Ping
+
@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PLAYER"]
+
@ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"]
+
@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]
+
@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_DEATHS"]
+
@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_RATIO"]
+
@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_SPM"]
+
@ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"]
+
@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PING"]
+
+ + + +
@client.Score
+
@(client.Kills ?? 0)
+
@(client.Deaths ?? 0)
+
@Math.Round(client.Kdr ?? 0, 2)
+
@Math.Round(client.ScorePerMinute ?? 0)
+
@(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))
+
@client.Ping
+
diff --git a/WebfrontCore/Views/Server/_Server.cshtml b/WebfrontCore/Views/Server/_Server.cshtml index e266fd0ff..19647966b 100644 --- a/WebfrontCore/Views/Server/_Server.cshtml +++ b/WebfrontCore/Views/Server/_Server.cshtml @@ -18,65 +18,70 @@ } } -
-
- - - - - - @if (ViewBag.Authorized) - { - - } - - - -
- -
- @Model.Map - @if (!string.IsNullOrEmpty(Model.GameType) && Model.GameType.Length > 1) - { - - @Model.GameType - } -
-
- @if (Model.LobbyZScore != null) - { -
- @(Model.LobbyZScore ?? 0) - +
+
+ +
+ @Model.Map + @if (!string.IsNullOrEmpty(Model.GameType) && Model.GameType.Length > 1) + { + + @Model.GameType + } +
+
+ @if (Model.LobbyZScore != null) + { +
+ @(Model.LobbyZScore ?? 0) + +
+ } +
+ @Model.ClientCount/@Model.MaxClients
- } -
- @Model.ClientCount/@Model.MaxClients
- @if (ViewBag.Authorized) +
+ +
+ + @if (Model.Players.Any()) { -
- +
+
} - - - -
-
- @await Html.PartialAsync("../Server/_ClientActivity", Model) -
- -
-
- +
+
+ +
diff --git a/WebfrontCore/Views/Shared/Components/Client/_RecentClients.cshtml b/WebfrontCore/Views/Shared/Components/Client/_RecentClients.cshtml index 734809226..d85f4173c 100644 --- a/WebfrontCore/Views/Shared/Components/Client/_RecentClients.cshtml +++ b/WebfrontCore/Views/Shared/Components/Client/_RecentClients.cshtml @@ -19,7 +19,7 @@ { - @client.Name + @client.Name @client.IPAddress @@ -39,7 +39,7 @@ {
- +
diff --git a/WebfrontCore/Views/Shared/Components/ProfileMetaList/_List.cshtml b/WebfrontCore/Views/Shared/Components/ProfileMetaList/_List.cshtml index d80d31f09..d666aa8d8 100644 --- a/WebfrontCore/Views/Shared/Components/ProfileMetaList/_List.cshtml +++ b/WebfrontCore/Views/Shared/Components/ProfileMetaList/_List.cshtml @@ -6,9 +6,9 @@ var lastHeaderEventDate = DateTime.UtcNow; - TimeSpan timeSpanForEvent(DateTime When) + TimeSpan TimeSpanForEvent(DateTime occuredAt) { - var timePassed = (DateTime.UtcNow - When); + var timePassed = DateTime.UtcNow - occuredAt; var daysPassed = timePassed.TotalDays; var minutesPassed = timePassed.TotalMinutes; @@ -22,45 +22,43 @@ return TimeSpan.FromHours(1); } - if (daysPassed > 1 && daysPassed <= 7) + if (daysPassed is > 1 and <= 7) { return TimeSpan.FromDays(1); } - if (daysPassed > 7 && daysPassed <= 31) + if (daysPassed is > 7 and <= 31) { return TimeSpan.FromDays(31); } - if (daysPassed > 31 && daysPassed <= 365) + if (daysPassed is > 31 and <= 365) { return TimeSpan.FromDays(31); } - else - { - return TimeSpan.FromDays(365); - } + return TimeSpan.FromDays(365); } } -@if (Model.Count() == 0) +@{ var start = 0; } +@foreach (var meta in Model.OrderByDescending(meta => meta.When)) { -
@ViewBag.Localization["WEBFRONT_CLIENT_META_NONE"]
-} - -@foreach (var meta in Model.OrderByDescending(_meta => _meta.When)) -{ - @if ((lastHeaderEventDate - meta.When) > timeSpanForEvent(lastHeaderEventDate)) + if (lastHeaderEventDate - meta.When > TimeSpanForEvent(lastHeaderEventDate) && (start > 0 || (DateTime.UtcNow - meta.When).TotalDays > 3)) { -
- - @meta.When.HumanizeForCurrentCulture() -
+ TempData["ShowMetaHeader"] = true; lastHeaderEventDate = meta.When; } + else + { + TempData["ShowMetaHeader"] = false; + } + start++; -
- +
+ +
} diff --git a/WebfrontCore/Views/Shared/Error.cshtml b/WebfrontCore/Views/Shared/Error.cshtml index 95c2db318..877f967a7 100644 --- a/WebfrontCore/Views/Shared/Error.cshtml +++ b/WebfrontCore/Views/Shared/Error.cshtml @@ -1,15 +1,16 @@ @model Exception -@using SharedLibraryCore @{ ViewData["Title"] = "Error"; } -

@SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_TITLE"]

-

@SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_DESC"]

- - @if (Model != null) - { - @SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_CODE"].FormatExt(Model.Message); - } - +
+

@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_TITLE"]

+

@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_DESC"]

+ + @if (Model != null) + { + @Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_CODE"].FormatExt(Model.Message) + } + +
diff --git a/WebfrontCore/Views/Shared/ResponseStatusCode.cshtml b/WebfrontCore/Views/Shared/ResponseStatusCode.cshtml index 0a79e1641..1c36daf20 100644 --- a/WebfrontCore/Views/Shared/ResponseStatusCode.cshtml +++ b/WebfrontCore/Views/Shared/ResponseStatusCode.cshtml @@ -2,12 +2,20 @@ @{ ViewData["Title"] = "Error"; } - -

@SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_TITLE"]

-

@SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_DESC"]

- - @if (Model.HasValue && Model.Value == 404) - { - @SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_NOTFOUND"] - } - \ No newline at end of file +
+
+
+
+
+

@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_TITLE"]

+

@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_GENERIC_DESC"]

+
+ @if (Model is 404) + { + @Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ERROR_NOTFOUND"] + } +
+
+
+
+
diff --git a/WebfrontCore/Views/Shared/_DataTable.cshtml b/WebfrontCore/Views/Shared/_DataTable.cshtml new file mode 100644 index 000000000..4139a730e --- /dev/null +++ b/WebfrontCore/Views/Shared/_DataTable.cshtml @@ -0,0 +1,69 @@ +@model WebfrontCore.ViewModels.TableInfo +@{ + Layout = null; +} + +

+ +

+ + + + + @foreach (var column in Model.Columns) + { + + } + + + + @{ var start = 0;} + @if (!Model.Rows.Any()) + { + + + + + + + + + + } + @foreach (var row in Model.Rows) + { + + + @for (var i = 0; i < Model.Columns.Count; i++) + { + + } + + + + + + + + start++; + } + +
@column.Title
No data...
+ — + No data...
@row.Datum[i]
+ @foreach (var column in Model.Columns) + { +
@column.Title
+ } +
+ @for (var i = 0; i < Model.Columns.Count; i++) + { +
@row.Datum[i]
+ } +
+@if (Model.InitialRowCount > 0 && Model.Rows.Count > 0) +{ + +} diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index 1cfb2e51e..69908ecc2 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -1,10 +1,7 @@ -@{ - var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex; -} - + - + @ViewBag.Title | IW4MAdmin @@ -16,111 +13,109 @@ - - + + + @if (ViewBag.Configuration.WebfrontPrimaryColor is not null) + { + + } + @if (ViewBag.Configuration.WebfrontSecondaryColor is not null) + { + + } - + @await RenderSectionAsync("styles", false) - -
- -
+
+
+
- -
+
+ +
+ + + + -