Compare commits

...

28 Commits

Author SHA1 Message Date
16d75470b5 fix login persistence issue 2022-06-15 21:00:01 -05:00
f02552faa1 fix up query/check 2022-06-15 20:19:22 -05:00
a4923d03f9 hide token generation button for non-logged-in users 2022-06-15 19:39:53 -05:00
8ae6561f4e update schema to support unique guid + game combinations 2022-06-15 19:37:34 -05:00
deeb1dea87 set the rcon parser game name for retail WaW 2022-06-14 15:12:19 -05:00
9ab34614c5 don't publish disconnect event if no client id 2022-06-14 15:00:23 -05:00
2cff25d6b3 make alert menu scrollable for large # of alerts 2022-06-13 11:03:39 -05:00
df3e226dc9 actually fix the previous issue 2022-06-12 16:37:07 -05:00
ef3db63ba7 fix issue that shouldn't actually be an issue 2022-06-12 15:09:26 -05:00
49fe4520ff improve alert display for mobile 2022-06-12 12:20:08 -05:00
6587187a34 fix memory/database leak with ranked player count cache 2022-06-12 12:19:32 -05:00
b337e232a2 use bot ip address when determining if client is bot 2022-06-12 10:09:56 -05:00
a44b4e9475 add alert/notification functionality (for server connection events and messages) 2022-06-11 11:34:00 -05:00
ffb0e5cac1 update for t5 dvar format change 2022-06-11 09:56:28 -05:00
ecc2b5bf54 increase width of side context menu for longer server names 2022-06-09 13:59:00 -05:00
2ac9cc4379 fix bug with loading top stats for individual servers 2022-06-09 13:50:58 -05:00
215037095f remove extra parenthesis oops.. 2022-06-09 10:15:43 -05:00
5433d7d1d2 add total ranked client number for stats pages 2022-06-09 09:56:41 -05:00
0446fe1ec5 revert time out for status preventing server from entering unreachable state 2022-06-08 09:10:31 -05:00
cf2a00e5b3 add game to player profile and admins page 2022-06-07 21:58:32 -05:00
ab494a22cb add mwr to game list (h1) 2022-06-07 12:10:39 -05:00
b690579154 fix issue with meta event context after 1st page load 2022-06-05 16:35:39 -05:00
acc967e50a add ban management page 2022-06-05 16:27:56 -05:00
c493fbe13d add game badge to server overview 2022-06-04 09:58:30 -05:00
ee56a5db1f fix map/gametype alignment on server overview and add back ip display on connect click 2022-06-04 09:21:08 -05:00
f235d0fafd update for pluto t5 rcon issue 2022-06-03 17:01:58 -05:00
7ecf516278 add plutonium T5 parser. Must use ManualLogPath 2022-06-03 16:26:58 -05:00
210f1ca336 fix incorrect wildcard colorcode 2022-06-02 19:59:09 -05:00
102 changed files with 11822 additions and 373 deletions

View File

@ -0,0 +1,55 @@
using System;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.Alerts;
public static class AlertExtensions
{
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
{
return new Alert.AlertState
{
RecipientId = client.ClientId,
Category = type ?? Alert.AlertCategory.Information
};
}
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
{
state.Category = category;
return state;
}
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
{
state.Type = type;
return state;
}
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
{
state.Message = message;
return state;
}
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
{
state.ExpiresAt = DateTime.Now.Add(expiration);
return state;
}
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
{
state.Source = source;
return state;
}
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
{
state.Source = client.Name.StripColors();
state.SourceId = client.ClientId;
return state;
}
}

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Alerts;
public class AlertManager : IAlertManager
{
private readonly ApplicationConfiguration _appConfig;
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
public AlertManager(ApplicationConfiguration appConfig)
{
_appConfig = appConfig;
_states.TryAdd(0, new List<Alert.AlertState>());
}
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
public async Task Initialize()
{
foreach (var source in _staticSources)
{
var alerts = await source();
foreach (var alert in alerts)
{
AddAlert(alert);
}
}
}
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
{
lock (_states)
{
var alerts = Enumerable.Empty<Alert.AlertState>();
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
{
alerts = alerts.Concat(_states[0].Where(alert =>
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
}
if (_states.ContainsKey(client.ClientId))
{
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
}
return alerts.OrderByDescending(alert => alert.OccuredAt);
}
}
public void MarkAlertAsRead(Guid alertId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
if (matchingEvent is null)
{
continue;
}
items.Remove(matchingEvent);
OnAlertConsumed?.Invoke(this, matchingEvent);
}
}
}
public void MarkAllAlertsAsRead(int recipientId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
items.RemoveAll(item =>
{
if (item.RecipientId != null && item.RecipientId != recipientId)
{
return false;
}
OnAlertConsumed?.Invoke(this, item);
return true;
});
}
}
}
public void AddAlert(Alert.AlertState alert)
{
lock (_states)
{
if (alert.RecipientId is null)
{
_states[0].Add(alert);
return;
}
if (!_states.ContainsKey(alert.RecipientId.Value))
{
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
}
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
{
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
}
_states[alert.RecipientId.Value].Add(alert);
PruneOldAlerts();
}
}
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
{
_staticSources.Add(alertSource);
}
private void PruneOldAlerts()
{
foreach (var value in _states.Values)
{
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
}
}
}

View File

@ -57,6 +57,7 @@ namespace IW4MAdmin.Application
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
@ -82,13 +83,14 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = clientService; ClientSvc = clientService;
PenaltySvc = penaltyService; PenaltySvc = penaltyService;
_alertManager = alertManager;
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
@ -508,6 +510,7 @@ namespace IW4MAdmin.Application
#endregion #endregion
_metaRegistration.Register(); _metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS #region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -629,9 +632,9 @@ namespace IW4MAdmin.Application
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList(); return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
} }
public EFClient FindActiveClient(EFClient client) =>client.ClientNumber < 0 ? public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
GetActiveClients() GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client : .FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
client; client;
public ClientService GetClientService() public ClientService GetClientService()
@ -697,5 +700,6 @@ namespace IW4MAdmin.Application
} }
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
} }
} }

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Misc; using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024; private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout, public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout) IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
: base(config, layout)
{ {
Name = "offlinemessage"; Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"]; Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om"; Alias = "om";
Permission = EFClient.Permission.Moderator; Permission = EFClient.Permission.Moderator;
RequiresTarget = true; RequiresTarget = true;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
_alertManager = alertManager;
_alertManager.RegisterStaticAlertSource(async () =>
{
var context = contextFactory.CreateContext(false);
return await context.InboxMessages.Where(message => !message.IsDelivered)
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
.Select(message => new Alert.AlertState
{
OccuredAt = message.CreatedDateTime,
Message = message.Message,
ExpiresAt = DateTime.UtcNow.AddDays(7),
Category = Alert.AlertCategory.Message,
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
SourceId = message.SourceClientId,
RecipientId = message.DestinationClientId,
ReferenceId = message.InboxMessageId,
Type = nameof(EFInboxMessage)
}).ToListAsync();
});
_alertManager.OnAlertConsumed += (_, state) =>
{
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
{
return;
}
try
{
var context = contextFactory.CreateContext(true);
foreach (var message in context.InboxMessages
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
{
message.IsDelivered = true;
}
context.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
}
};
} }
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return; return;
} }
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId) if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{ {
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return; return;
} }
if (gameEvent.Target.IsIngame) if (gameEvent.Target.IsIngame)
{ {
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
.FormatExt(gameEvent.Target.Name));
return; return;
} }
await using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString()); var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage() var newMessage = new EFInboxMessage
{ {
SourceClientId = gameEvent.Origin.ClientId, SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId, DestinationClientId = gameEvent.Target.ClientId,
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
Message = gameEvent.Data, Message = gameEvent.Data,
}; };
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
.WithMessage(gameEvent.Data.Trim())
.FromClient(gameEvent.Origin)
.OfType(nameof(EFInboxMessage))
.ExpiresIn(TimeSpan.FromDays(7)));
try try
{ {
context.Set<EFInboxMessage>().Add(newMessage); context.Set<EFInboxMessage>().Add(newMessage);
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
} }
} }
} }
} }

View File

@ -24,8 +24,10 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models; using Data.Models;
using Data.Models.Server; using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands; using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
namespace IW4MAdmin namespace IW4MAdmin
@ -73,7 +75,7 @@ namespace IW4MAdmin
{ {
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber); ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId); var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
// first time client is connecting to server // first time client is connecting to server
if (client == null) if (client == null)
@ -116,7 +118,7 @@ namespace IW4MAdmin
public override async Task OnClientDisconnected(EFClient client) public override async Task OnClientDisconnected(EFClient client)
{ {
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId)) if (GetClientsAsList().All(eachClient => eachClient.NetworkId != client.NetworkId))
{ {
using (LogContext.PushProperty("Server", ToString())) using (LogContext.PushProperty("Server", ToString()))
{ {
@ -306,8 +308,16 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")); Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Error)
.FromSource("System")
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
} }
Throttled = true; Throttled = true;
} }
@ -318,7 +328,15 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Information)
.FromSource("System")
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
} }
if (!string.IsNullOrEmpty(CustomSayName)) if (!string.IsNullOrEmpty(CustomSayName))
@ -431,7 +449,7 @@ namespace IW4MAdmin
Clients[E.Origin.ClientNumber] = E.Origin; Clients[E.Origin.ClientNumber] = E.Origin;
try try
{ {
E.Origin.GameName = (Reference.Game?)GameName; E.Origin.GameName = (Reference.Game)GameName;
E.Origin = await OnClientConnected(E.Origin); E.Origin = await OnClientConnected(E.Origin);
E.Target = E.Origin; E.Target = E.Origin;
} }
@ -499,7 +517,7 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin); E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId, await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.CurrentAlias?.IPAddress); E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty); await Manager.GetPenaltyService().Create(unflagPenalty);
} }
@ -745,7 +763,7 @@ namespace IW4MAdmin
private async Task OnClientUpdate(EFClient origin) private async Task OnClientUpdate(EFClient origin)
{ {
var client = Manager.GetActiveClients().FirstOrDefault(c => c.NetworkId == origin.NetworkId); var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
if (client == null) if (client == null)
{ {
@ -790,12 +808,10 @@ namespace IW4MAdmin
/// array index 2 = updated clients /// array index 2 = updated clients
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync() async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{ {
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(tokenSource.Token); var statusResponse = await this.GetStatusAsync(token);
if (statusResponse is null) if (statusResponse is null)
{ {
@ -918,11 +934,11 @@ namespace IW4MAdmin
private DateTime _lastMessageSent = DateTime.Now; private DateTime _lastMessageSent = DateTime.Now;
private DateTime _lastPlayerCount = DateTime.Now; private DateTime _lastPlayerCount = DateTime.Now;
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts) public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
{ {
try try
{ {
if (cts.IsCancellationRequested) if (token.IsCancellationRequested)
{ {
await ShutdownInternal(); await ShutdownInternal();
return true; return true;
@ -936,7 +952,7 @@ namespace IW4MAdmin
return true; return true;
} }
var polledClients = await PollPlayersAsync(); var polledClients = await PollPlayersAsync(token);
if (polledClients is null) if (polledClients is null)
{ {
@ -947,7 +963,7 @@ namespace IW4MAdmin
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */)) .Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{ {
disconnectingClient.CurrentServer = this; disconnectingClient.CurrentServer = this;
var e = new GameEvent() var e = new GameEvent
{ {
Type = GameEvent.EventType.PreDisconnect, Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient, Origin = disconnectingClient,
@ -964,7 +980,7 @@ namespace IW4MAdmin
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot))) !string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
{ {
client.CurrentServer = this; client.CurrentServer = this;
client.GameName = (Reference.Game?)GameName; client.GameName = (Reference.Game)GameName;
var e = new GameEvent var e = new GameEvent
{ {
@ -1514,7 +1530,7 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString()); ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.User, originClient); targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId, await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress); targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty); await Manager.GetPenaltyService().Create(unbanPenalty);
} }

View File

@ -27,6 +27,7 @@ using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Helpers; using Data.Helpers;
using Integrations.Source.Extensions; using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization; using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -448,6 +449,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IServerDataCollector, ServerDataCollector>() .AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>() .AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) .AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>() .AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);

View File

@ -29,7 +29,7 @@ namespace IW4MAdmin.Application.Misc
OnClientConnect?.Invoke(this, gameEvent); OnClientConnect?.Invoke(this, gameEvent);
} }
if (gameEvent.Type == GameEvent.EventType.Disconnect) if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
{ {
OnClientDisconnect?.Invoke(this, gameEvent); OnClientDisconnect?.Invoke(this, gameEvent);
} }
@ -41,4 +41,4 @@ namespace IW4MAdmin.Application.Misc
} }
} }
} }
} }

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server; using Data.Models.Server;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache; private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache; private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache; private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan = private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10); Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache, public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache, IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache) IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{ {
_logger = logger; _logger = logger;
_snapshotCache = snapshotCache; _snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache; _serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache; _clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
} }
public async Task<(int?, DateTime?)> public async Task<(int?, DateTime?)>
@ -160,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
return Enumerable.Empty<ClientHistoryInfo>(); return Enumerable.Empty<ClientHistoryInfo>();
} }
} }
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return await set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == serverId)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
return 0;
}
}
} }
} }

View File

@ -9,40 +9,42 @@ namespace IW4MAdmin.Application.Misc
{ {
internal class TokenAuthentication : ITokenAuthentication internal class TokenAuthentication : ITokenAuthentication
{ {
private readonly ConcurrentDictionary<long, TokenState> _tokens; private readonly ConcurrentDictionary<string, TokenState> _tokens;
private readonly RandomNumberGenerator _random; private readonly RandomNumberGenerator _random;
private static readonly TimeSpan TimeoutPeriod = new TimeSpan(0, 0, 120); private static readonly TimeSpan TimeoutPeriod = new(0, 0, 120);
private const short TokenLength = 4; private const short TokenLength = 4;
public TokenAuthentication() public TokenAuthentication()
{ {
_tokens = new ConcurrentDictionary<long, TokenState>(); _tokens = new ConcurrentDictionary<string, TokenState>();
_random = RandomNumberGenerator.Create(); _random = RandomNumberGenerator.Create();
} }
public bool AuthorizeToken(long networkId, string token) public bool AuthorizeToken(ITokenIdentifier authInfo)
{ {
var authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token; var key = BuildKey(authInfo);
var authorizeSuccessful = _tokens.ContainsKey(key) && _tokens[key].Token == key;
if (authorizeSuccessful) if (authorizeSuccessful)
{ {
_tokens.TryRemove(networkId, out _); _tokens.TryRemove(key, out _);
} }
return authorizeSuccessful; return authorizeSuccessful;
} }
public TokenState GenerateNextToken(long networkId) public TokenState GenerateNextToken(ITokenIdentifier authInfo)
{ {
TokenState state; TokenState state;
var genKey = BuildKey(authInfo);
if (_tokens.ContainsKey(networkId)) if (_tokens.ContainsKey(genKey))
{ {
state = _tokens[networkId]; state = _tokens[genKey];
if ((DateTime.Now - state.RequestTime) > TimeoutPeriod) if (DateTime.Now - state.RequestTime > TimeoutPeriod)
{ {
_tokens.TryRemove(networkId, out _); _tokens.TryRemove(genKey, out _);
} }
else else
@ -53,12 +55,11 @@ namespace IW4MAdmin.Application.Misc
state = new TokenState state = new TokenState
{ {
NetworkId = networkId,
Token = _generateToken(), Token = _generateToken(),
TokenDuration = TimeoutPeriod TokenDuration = TimeoutPeriod
}; };
_tokens.TryAdd(networkId, state); _tokens.TryAdd(genKey, state);
// perform some housekeeping so we don't have built up tokens if they're not ever used // perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens) foreach (var (key, value) in _tokens)
@ -96,5 +97,7 @@ namespace IW4MAdmin.Application.Misc
_random.Dispose(); _random.Dispose();
return token.ToString(); return token.ToString();
} }
private string BuildKey(ITokenIdentifier authInfo) => $"{authInfo.NetworkId}_${authInfo.Game}";
} }
} }

View File

@ -20,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
public class BaseRConParser : IRConParser public class BaseRConParser : IRConParser
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private static string _botIpIndicator = "00000000.";
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory) public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
{ {
@ -290,8 +291,15 @@ namespace IW4MAdmin.Application.RConParsers
long networkId; long networkId;
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString; string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP(); var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]]
.Contains(_botIpIndicator))
{
ip = System.Net.IPAddress.Broadcast.ToString().ConvertToIP();
}
try try
{ {
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
@ -306,9 +314,9 @@ namespace IW4MAdmin.Application.RConParsers
continue; continue;
} }
var client = new EFClient() var client = new EFClient
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias
{ {
Name = name, Name = name,
IPAddress = ip IPAddress = ip

View File

@ -46,7 +46,7 @@ namespace IW4MAdmin.Application.RConParsers
{ColorCodes.White.ToString(), "^7"}, {ColorCodes.White.ToString(), "^7"},
{ColorCodes.Map.ToString(), "^8"}, {ColorCodes.Map.ToString(), "^8"},
{ColorCodes.Grey.ToString(), "^9"}, {ColorCodes.Grey.ToString(), "^9"},
{ColorCodes.Wildcard.ToString(), ":^"}, {ColorCodes.Wildcard.ToString(), "^:"}
}; };
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -9,6 +10,11 @@ namespace Data.Abstractions
{ {
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName, void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false); TimeSpan? expirationTime = null, bool autoRefresh = false);
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default); Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
} }
} }

View File

@ -85,7 +85,15 @@ namespace Data.Context
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
// make network id unique // make network id unique
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); }); modelBuilder.Entity<EFClient>(entity =>
{
entity.HasIndex(e => e.NetworkId);
entity.HasAlternateKey(client => new
{
client.NetworkId,
client.GameName
});
});
modelBuilder.Entity<EFPenalty>(entity => modelBuilder.Entity<EFPenalty>(entity =>
{ {

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
@ -15,8 +17,8 @@ namespace Data.Helpers
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates = private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
new ConcurrentDictionary<string, CacheState<TReturnType>>(); private readonly object _defaultKey = new();
private bool _autoRefresh; private bool _autoRefresh;
private const int DefaultExpireMinutes = 15; private const int DefaultExpireMinutes = 15;
@ -51,41 +53,61 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key, public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false) TimeSpan? expirationTime = null, bool autoRefresh = false)
{ {
if (_cacheStates.ContainsKey(key)) SetCacheItem(getter, key, null, expirationTime, autoRefresh);
{
_logger.LogDebug("Cache key {Key} is already added", key);
return;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
} }
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{
ids ??= new[] { _defaultKey };
if (!_cacheStates.ContainsKey(key))
{
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
}
foreach (var id in ids)
{
if (_cacheStates[key].ContainsKey(id))
{
continue;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_cacheStates[key].Add(id, state);
_autoRefresh = autoRefresh;
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
}
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
await GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
CancellationToken cancellationToken = default)
{ {
if (!_cacheStates.ContainsKey(keyName)) if (!_cacheStates.ContainsKey(keyName))
{ {
throw new ArgumentException("No cache found for key {key}", keyName); throw new ArgumentException("No cache found for key {key}", keyName);
} }
var state = _cacheStates[keyName]; var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
// when auto refresh is off we want to check the expiration and value // when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically // when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
@ -115,4 +137,4 @@ namespace Data.Helpers
} }
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.Sql("UPDATE `EFClients` set `GameName` = 0 WHERE `GameName` IS NULL");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "int",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

View File

@ -64,7 +64,7 @@ namespace Data.Migrations.MySql
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<int?>("GameName") b.Property<int>("GameName")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
@ -90,12 +90,13 @@ namespace Data.Migrations.MySql
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -456,6 +457,8 @@ namespace Data.Migrations.MySql
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.Sql("UPDATE \"EFClients\" SET \"GameName\" = 0 WHERE \"GameName\" IS NULL");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

View File

@ -71,7 +71,7 @@ namespace Data.Migrations.Postgresql
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<int?>("GameName") b.Property<int>("GameName")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
@ -97,12 +97,13 @@ namespace Data.Migrations.Postgresql
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -475,6 +476,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

View File

@ -62,7 +62,7 @@ namespace Data.Migrations.Sqlite
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("GameName") b.Property<int>("GameName")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
@ -88,12 +88,13 @@ namespace Data.Migrations.Sqlite
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -454,6 +455,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");

View File

@ -63,7 +63,7 @@ namespace Data.Models.Client
public DateTime FirstConnection { get; set; } public DateTime FirstConnection { get; set; }
[Required] [Required]
public DateTime LastConnection { get; set; } public DateTime LastConnection { get; set; }
public Reference.Game? GameName { get; set; } = Reference.Game.UKN; public Reference.Game GameName { get; set; } = Reference.Game.UKN;
public bool Masked { get; set; } public bool Masked { get; set; }
[Required] [Required]
public int AliasLinkId { get; set; } public int AliasLinkId { get; set; }

View File

@ -86,7 +86,8 @@ namespace Data.Models.Configuration
entity.HasIndex(ranking => ranking.Ranking); entity.HasIndex(ranking => ranking.Ranking);
entity.HasIndex(ranking => ranking.ZScore); entity.HasIndex(ranking => ranking.ZScore);
entity.HasIndex(ranking => ranking.UpdatedDateTime); entity.HasIndex(ranking => ranking.UpdatedDateTime);
entity.HasIndex(ranking => ranking.CreatedDateTime);
}); });
} }
} }
} }

View File

@ -15,7 +15,8 @@
T6 = 7, T6 = 7,
T7 = 8, T7 = 8,
SHG1 = 9, SHG1 = 9,
CSGO = 10 CSGO = 10,
H1 = 11
} }
public enum ConnectionType public enum ConnectionType
@ -24,4 +25,4 @@
Disconnect Disconnect
} }
} }
} }

View File

@ -54,6 +54,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"

View File

@ -368,7 +368,9 @@ namespace Integrations.Cod
throw new RConException("Unexpected response header from server"); throw new RConException("Unexpected response header from server");
} }
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
return splitResponse; return splitResponse;
} }

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -4,6 +4,7 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Plugins.Login.Commands namespace IW4MAdmin.Plugins.Login.Commands
{ {
@ -18,7 +19,7 @@ namespace IW4MAdmin.Plugins.Login.Commands
RequiresTarget = false; RequiresTarget = false;
Arguments = new CommandArgument[] Arguments = new CommandArgument[]
{ {
new CommandArgument() new()
{ {
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"], Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
Required = true Required = true
@ -26,24 +27,29 @@ namespace IW4MAdmin.Plugins.Login.Commands
}; };
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data); var success = gameEvent.Owner.Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
{
NetworkId = gameEvent.Origin.NetworkId,
Game = gameEvent.Origin.GameName,
Token = gameEvent.Data
});
if (!success) if (!success)
{ {
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt)); var hashedPassword = await Task.FromResult(Hashing.Hash(gameEvent.Data, gameEvent.Origin.PasswordSalt));
success = hashedPassword[0] == E.Origin.Password; success = hashedPassword[0] == gameEvent.Origin.Password;
} }
if (success) if (success)
{ {
Plugin.AuthorizedClients[E.Origin.ClientId] = true; Plugin.AuthorizedClients[gameEvent.Origin.ClientId] = true;
} }
_ = success ? _ = success ?
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) : gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]); gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
} }
} }
} }

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -0,0 +1,39 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
name: 'Plutonium T5 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n)?\\w*(.+)*$';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
rconParser.GameName = 6; // T5
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
eventParser.GameName = 6; // T5
eventParser.Configuration.GuidNumberStyle = 7; // Integer
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.2, version: 0.3,
name: 'Call of Duty 5: World at War Parser', name: 'Call of Duty 5: World at War Parser',
isParser: true, isParser: true,
@ -17,6 +17,7 @@ var plugin = {
rconParser.Configuration.GuidNumberStyle = 7; // Integer rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 28960; rconParser.Configuration.DefaultRConPort = 28960;
rconParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86'; rconParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86';
rconParser.GameName = 5; // T4
eventParser.Configuration.GuidNumberStyle = 7; // Integer eventParser.Configuration.GuidNumberStyle = 7; // Integer
eventParser.GameName = 5; // T4 eventParser.GameName = 5; // T4

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
@ -88,8 +89,8 @@ namespace Stats.Client
return zScore ?? 0; return zScore ?? 0;
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30)); }, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
await _distributionCache.GetCacheItem(DistributionCacheKey); await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey); await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
/*foreach (var serverId in _serverIds) /*foreach (var serverId in _serverIds)
{ {
@ -132,7 +133,7 @@ namespace Stats.Client
public async Task<double> GetZScoreForServer(long serverId, double value) public async Task<double> GetZScoreForServer(long serverId, double value)
{ {
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey); var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
if (!serverParams.ContainsKey(serverId)) if (!serverParams.ContainsKey(serverId))
{ {
return 0.0; return 0.0;
@ -150,7 +151,7 @@ namespace Stats.Client
public async Task<double?> GetRatingForZScore(double? value) public async Task<double?> GetRatingForZScore(double? value)
{ {
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey); var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore); return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
} }
} }

View File

@ -79,7 +79,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
else else
{ {
gameEvent.Owner.Broadcast(topStats); await gameEvent.Owner.BroadcastAsync(topStats);
} }
} }
} }

View File

@ -14,6 +14,7 @@ namespace Stats.Dtos
public EFClient.Permission Level { get; set; } public EFClient.Permission Level { get; set; }
public double? Performance { get; set; } public double? Performance { get; set; }
public int? Ranking { get; set; } public int? Ranking { get; set; }
public int TotalRankedClients { get; set; }
public double? ZScore { get; set; } public double? ZScore { get; set; }
public double? Rating { get; set; } public double? Rating { get; set; }
public List<ServerInfo> Servers { get; set; } public List<ServerInfo> Servers { get; set; }
@ -25,4 +26,4 @@ namespace Stats.Dtos
public List<EFClientRankingHistory> Ratings { get; set; } public List<EFClientRankingHistory> Ratings { get; set; }
public List<EFClientStatistics> LegacyStats { get; set; } public List<EFClientStatistics> LegacyStats { get; set; }
} }
} }

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats; using IW4MAdmin.Plugins.Stats;
@ -12,7 +13,6 @@ using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using Stats.Client.Abstractions;
using Stats.Dtos; using Stats.Dtos;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -50,7 +50,8 @@ namespace Stats.Helpers
{ {
client.ClientId, client.ClientId,
client.CurrentAlias.Name, client.CurrentAlias.Name,
client.Level client.Level,
client.GameName
}).FirstOrDefaultAsync(client => client.ClientId == query.ClientId); }).FirstOrDefaultAsync(client => client.ClientId == query.ClientId);
if (clientInfo == null) if (clientInfo == null)
@ -111,8 +112,9 @@ namespace Stats.Helpers
Rating = mostRecentRanking?.PerformanceMetric, Rating = mostRecentRanking?.PerformanceMetric,
All = hitStats, All = hitStats,
Servers = _manager.GetServers() Servers = _manager.GetServers()
.Select(server => new ServerInfo() .Select(server => new ServerInfo
{Name = server.Hostname, IPAddress = server.IP, Port = server.Port}) {Name = server.Hostname, IPAddress = server.IP, Port = server.Port, Game = (Reference.Game)server.GameName})
.Where(server => server.Game == clientInfo.GameName)
.ToList(), .ToList(),
Aggregate = hitStats.FirstOrDefault(hit => Aggregate = hitStats.FirstOrDefault(hit =>
hit.HitLocationId == null && hit.ServerId == serverId && hit.WeaponId == null && hit.HitLocationId == null && hit.ServerId == serverId && hit.WeaponId == null &&
@ -153,4 +155,4 @@ namespace Stats.Helpers
&& (zScore == null || stats.ZScore > zScore); && (zScore == null || stats.ZScore > zScore);
} }
} }
} }

View File

@ -42,10 +42,11 @@ namespace IW4MAdmin.Plugins.Stats
private readonly ILogger<Plugin> _logger; private readonly ILogger<Plugin> _logger;
private readonly List<IClientStatisticCalculator> _statCalculators; private readonly List<IClientStatisticCalculator> _statCalculators;
private readonly IServerDistributionCalculator _serverDistributionCalculator; private readonly IServerDistributionCalculator _serverDistributionCalculator;
private readonly IServerDataViewer _serverDataViewer;
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory, public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger, ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger,
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator) IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer)
{ {
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings"); Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory; _databaseContextFactory = databaseContextFactory;
@ -56,6 +57,7 @@ namespace IW4MAdmin.Plugins.Stats
_logger = logger; _logger = logger;
_statCalculators = statCalculators.ToList(); _statCalculators = statCalculators.ToList();
_serverDistributionCalculator = serverDistributionCalculator; _serverDistributionCalculator = serverDistributionCalculator;
_serverDataViewer = serverDataViewer;
} }
public async Task OnEventAsync(GameEvent gameEvent, Server server) public async Task OnEventAsync(GameEvent gameEvent, Server server)
@ -201,13 +203,17 @@ namespace IW4MAdmin.Plugins.Stats
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed); var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2); var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1); var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
var overallRanking = await Manager.GetClientOverallRanking(request.ClientId);
return new List<InformationResponse> return new List<InformationResponse>
{ {
new InformationResponse new InformationResponse
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"], Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)), Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"].FormatExt((overallRanking == 0 ? "--" :
overallRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))),
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
),
Column = 0, Column = 0,
Order = 0, Order = 0,
Type = MetaType.Information Type = MetaType.Information

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,33 @@
using System;
using Data.Models.Client;
namespace SharedLibraryCore.Alerts;
public class Alert
{
public enum AlertCategory
{
Information,
Warning,
Error,
Message,
}
public class AlertState
{
public Guid AlertId { get; } = Guid.NewGuid();
public AlertCategory Category { get; set; }
public DateTime OccuredAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
public string Message { get; set; }
public string Source { get; set; }
public int? RecipientId { get; set; }
public int? SourceId { get; set; }
public int? ReferenceId { get; set; }
public bool? Delivered { get; set; }
public bool? Consumed { get; set; }
public EFClient.Permission? MinimumPermission { get; set; }
public string Type { get; set; }
public static AlertState Build() => new();
}
}

View File

@ -4,7 +4,6 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Context;
using Data.Models; using Data.Models;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
@ -20,28 +19,37 @@ namespace SharedLibraryCore
{ {
public class BaseController : Controller public class BaseController : Controller
{ {
protected readonly IAlertManager AlertManager;
/// <summary> /// <summary>
/// life span in months /// life span in months
/// </summary> /// </summary>
private const int COOKIE_LIFESPAN = 3; private const int CookieLifespan = 3;
private static readonly byte[] LocalHost = { 127, 0, 0, 1 }; private static readonly byte[] LocalHost = { 127, 0, 0, 1 };
private static string SocialLink; private static string _socialLink;
private static string SocialTitle; private static string _socialTitle;
protected readonly DatabaseContext Context;
protected List<Page> Pages; protected List<Page> Pages;
protected List<string> PermissionsSet; protected List<string> PermissionsSet;
protected bool Authorized { get; set; }
protected TranslationLookup Localization { get; }
protected EFClient Client { get; }
protected ApplicationConfiguration AppConfig { get; }
public IManager Manager { get; }
public BaseController(IManager manager) public BaseController(IManager manager)
{ {
AlertManager = manager.AlertManager;
Manager = manager; Manager = manager;
Localization ??= Utilities.CurrentLocalization.LocalizationIndex; Localization = Utilities.CurrentLocalization.LocalizationIndex;
AppConfig = Manager.GetApplicationSettings().Configuration(); AppConfig = Manager.GetApplicationSettings().Configuration();
if (AppConfig.EnableSocialLink && SocialLink == null) if (AppConfig.EnableSocialLink && _socialLink == null)
{ {
SocialLink = AppConfig.SocialLinkAddress; _socialLink = AppConfig.SocialLinkAddress;
SocialTitle = AppConfig.SocialLinkTitle; _socialTitle = AppConfig.SocialLinkTitle;
} }
Pages = Manager.GetPageList().Pages Pages = Manager.GetPageList().Pages
@ -56,7 +64,7 @@ namespace SharedLibraryCore
ViewBag.EnableColorCodes = AppConfig.EnableColorCodes; ViewBag.EnableColorCodes = AppConfig.EnableColorCodes;
ViewBag.Language = Utilities.CurrentLocalization.Culture.TwoLetterISOLanguageName; ViewBag.Language = Utilities.CurrentLocalization.Culture.TwoLetterISOLanguageName;
Client ??= new EFClient Client = new EFClient
{ {
ClientId = -1, ClientId = -1,
Level = Data.Models.Client.EFClient.Permission.Banned, Level = Data.Models.Client.EFClient.Permission.Banned,
@ -64,11 +72,7 @@ namespace SharedLibraryCore
}; };
} }
public IManager Manager { get; }
protected bool Authorized { get; set; }
protected TranslationLookup Localization { get; }
protected EFClient Client { get; }
protected ApplicationConfiguration AppConfig { get; }
protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple) protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
{ {
@ -76,7 +80,7 @@ namespace SharedLibraryCore
new AuthenticationProperties new AuthenticationProperties
{ {
AllowRefresh = true, AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(COOKIE_LIFESPAN), ExpiresUtc = DateTime.UtcNow.AddMonths(CookieLifespan),
IsPersistent = true, IsPersistent = true,
IssuedUtc = DateTime.UtcNow IssuedUtc = DateTime.UtcNow
}); });
@ -96,7 +100,7 @@ namespace SharedLibraryCore
Client.ClientId = clientId; Client.ClientId = clientId;
Client.NetworkId = clientId == 1 Client.NetworkId = clientId == 1
? 0 ? 0
: User.Claims.First(_claim => _claim.Type == ClaimTypes.PrimarySid).Value : User.Claims.First(claim => claim.Type == ClaimTypes.PrimarySid).Value
.ConvertGuidToLong(NumberStyles.HexNumber); .ConvertGuidToLong(NumberStyles.HexNumber);
Client.Level = (Data.Models.Client.EFClient.Permission)Enum.Parse( Client.Level = (Data.Models.Client.EFClient.Permission)Enum.Parse(
typeof(Data.Models.Client.EFClient.Permission), typeof(Data.Models.Client.EFClient.Permission),
@ -104,6 +108,9 @@ namespace SharedLibraryCore
Client.CurrentAlias = new EFAlias Client.CurrentAlias = new EFAlias
{ Name = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value }; { Name = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value };
Authorized = Client.ClientId >= 0; Authorized = Client.ClientId >= 0;
Client.GameName =
Enum.Parse<Reference.Game>(User.Claims
.First(claim => claim.Type == ClaimTypes.PrimaryGroupSid).Value);
} }
} }
@ -131,6 +138,7 @@ namespace SharedLibraryCore
new Claim(ClaimTypes.Role, Client.Level.ToString()), new Claim(ClaimTypes.Role, Client.Level.ToString()),
new Claim(ClaimTypes.Sid, Client.ClientId.ToString()), new Claim(ClaimTypes.Sid, Client.ClientId.ToString()),
new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")), new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")),
new Claim(ClaimTypes.PrimaryGroupSid, Client.GameName.ToString())
}; };
var claimsIdentity = new ClaimsIdentity(claims, "login"); var claimsIdentity = new ClaimsIdentity(claims, "login");
SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait(); SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait();
@ -150,8 +158,8 @@ namespace SharedLibraryCore
ViewBag.Url = AppConfig.WebfrontUrl; ViewBag.Url = AppConfig.WebfrontUrl;
ViewBag.User = Client; ViewBag.User = Client;
ViewBag.Version = Manager.Version; ViewBag.Version = Manager.Version;
ViewBag.SocialLink = SocialLink ?? ""; ViewBag.SocialLink = _socialLink ?? "";
ViewBag.SocialTitle = SocialTitle; ViewBag.SocialTitle = _socialTitle;
ViewBag.Pages = Pages; ViewBag.Pages = Pages;
ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex; ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex;
ViewBag.CustomBranding = shouldUseCommunityName ViewBag.CustomBranding = shouldUseCommunityName
@ -169,6 +177,7 @@ namespace SharedLibraryCore
ViewBag.ReportCount = Manager.GetServers().Sum(server => ViewBag.ReportCount = Manager.GetServers().Sum(server =>
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24))); server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
ViewBag.PermissionsSet = PermissionsSet; ViewBag.PermissionsSet = PermissionsSet;
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
base.OnActionExecuting(context); base.OnActionExecuting(context);
} }

View File

@ -381,7 +381,7 @@ namespace SharedLibraryCore.Commands
{ {
// todo: don't do the lookup here // todo: don't do the lookup here
var penalties = await gameEvent.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(gameEvent.Target.AliasLinkId, var penalties = await gameEvent.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(gameEvent.Target.AliasLinkId,
gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.CurrentAlias.IPAddress); gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.GameName, gameEvent.Target.CurrentAlias.IPAddress);
if (penalties if (penalties
.FirstOrDefault(p => .FirstOrDefault(p =>
@ -897,7 +897,7 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var existingPenalties = await E.Owner.Manager.GetPenaltyService() var existingPenalties = await E.Owner.Manager.GetPenaltyService()
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.IPAddress); .GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.GameName, E.Target.IPAddress);
var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick); var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick);
if (penalty == null) if (penalty == null)

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models.Client; using Data.Models.Client;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace SharedLibraryCore.Commands namespace SharedLibraryCore.Commands
@ -19,13 +20,17 @@ namespace SharedLibraryCore.Commands
RequiresTarget = false; RequiresTarget = false;
} }
public override Task ExecuteAsync(GameEvent E) public override Task ExecuteAsync(GameEvent gameEvent)
{ {
var state = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId); var state = gameEvent.Owner.Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
E.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token, {
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", E.Origin.ClientId)); Game = gameEvent.Origin.GameName,
NetworkId = gameEvent.Origin.NetworkId
});
gameEvent.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token,
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", gameEvent.Origin.ClientId));
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Data.Models.Misc;
using Newtonsoft.Json; using Newtonsoft.Json;
using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration
{ Permission.Console.ToString(), new List<string> { "*" } } { Permission.Console.ToString(), new List<string> { "*" } }
}; };
public Dictionary<string, Permission> MinimumAlertPermissions { get; set; } = new()
{
{ nameof(EFInboxMessage), Permission.Trusted },
{ GameEvent.EventType.ConnectionLost.ToString(), Permission.Administrator },
{ GameEvent.EventType.ConnectionRestored.ToString(), Permission.Administrator }
};
[ConfigurationIgnore] [ConfigurationIgnore]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new() public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()

View File

@ -1,4 +1,5 @@
using System; using System;
using Data.Models;
using Data.Models.Client; using Data.Models.Client;
namespace SharedLibraryCore.Dtos namespace SharedLibraryCore.Dtos
@ -10,6 +11,7 @@ namespace SharedLibraryCore.Dtos
public int LinkId { get; set; } public int LinkId { get; set; }
public EFClient.Permission Level { get; set; } public EFClient.Permission Level { get; set; }
public DateTime LastConnection { get; set; } public DateTime LastConnection { get; set; }
public Reference.Game Game { get; set; }
public bool IsMasked { get; set; } public bool IsMasked { get; set; }
} }
} }

View File

@ -9,6 +9,7 @@ namespace SharedLibraryCore.Dtos
public class PlayerInfo public class PlayerInfo
{ {
public string Name { get; set; } public string Name { get; set; }
public Reference.Game Game { get; set; }
public int ClientId { get; set; } public int ClientId { get; set; }
public string Level { get; set; } public string Level { get; set; }
public string Tag { get; set; } public string Tag { get; set; }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Data.Models;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
namespace SharedLibraryCore.Dtos namespace SharedLibraryCore.Dtos
@ -40,5 +41,6 @@ namespace SharedLibraryCore.Dtos
return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2); return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2);
} }
} }
public Reference.Game Game { get; set; }
} }
} }

View File

@ -0,0 +1,11 @@
using Data.Models;
using SharedLibraryCore.Interfaces;
namespace SharedLibraryCore.Helpers;
public class TokenIdentifier : ITokenIdentifier
{
public long NetworkId { get; set; }
public Reference.Game Game { get; set; }
public string Token { get; set; }
}

View File

@ -4,7 +4,6 @@ namespace SharedLibraryCore.Helpers
{ {
public sealed class TokenState public sealed class TokenState
{ {
public long NetworkId { get; set; }
public DateTime RequestTime { get; set; } = DateTime.Now; public DateTime RequestTime { get; set; } = DateTime.Now;
public TimeSpan TokenDuration { get; set; } public TimeSpan TokenDuration { get; set; }
public string Token { get; set; } public string Token { get; set; }
@ -12,4 +11,4 @@ namespace SharedLibraryCore.Helpers
public string RemainingTime => Math.Round(-(DateTime.Now - RequestTime).Subtract(TokenDuration).TotalMinutes, 1) public string RemainingTime => Math.Round(-(DateTime.Now - RequestTime).Subtract(TokenDuration).TotalMinutes, 1)
.ToString(); .ToString();
} }
} }

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Interfaces;
public interface IAlertManager
{
/// <summary>
/// Initializes the manager
/// </summary>
/// <returns></returns>
Task Initialize();
/// <summary>
/// Get all the alerts for given client
/// </summary>
/// <param name="client">client to retrieve alerts for</param>
/// <returns></returns>
IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client);
/// <summary>
/// Trigger a new alert
/// </summary>
/// <param name="alert">Alert to trigger</param>
void AddAlert(Alert.AlertState alert);
/// <summary>
/// Marks an alert as read and removes it from the manager
/// </summary>
/// <param name="alertId">Id of the alert to mark as read</param>
void MarkAlertAsRead(Guid alertId);
/// <summary>
/// Mark all alerts intended for the given recipientId as read
/// </summary>
/// <param name="recipientId">Identifier of the recipient</param>
void MarkAllAlertsAsRead(int recipientId);
/// <summary>
/// Registers a static (persistent) event source eg datastore that
/// gets initialized at startup
/// </summary>
/// <param name="alertSource">Source action</param>
void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource);
/// <summary>
/// Fires when an alert has been consumed (dimissed)
/// </summary>
EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
}

View File

@ -10,7 +10,7 @@ namespace SharedLibraryCore.Interfaces
Task<T> Delete(T entity); Task<T> Delete(T entity);
Task<T> Update(T entity); Task<T> Update(T entity);
Task<T> Get(int entityID); Task<T> Get(int entityID);
Task<T> GetUnique(long entityProperty); Task<T> GetUnique(long entityProperty, object altKey);
Task<IList<T>> Find(Func<T, bool> expression); Task<IList<T>> Find(Func<T, bool> expression);
} }
} }

View File

@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
/// event executed when event has finished executing /// event executed when event has finished executing
/// </summary> /// </summary>
event EventHandler<GameEvent> OnGameEventExecuted; event EventHandler<GameEvent> OnGameEventExecuted;
IAlertManager AlertManager { get; }
} }
} }

View File

@ -37,5 +37,13 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null, Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null,
CancellationToken token = default); CancellationToken token = default);
/// <summary>
/// Retrieves the number of ranked clients for given server id
/// </summary>
/// <param name="serverId">ServerId to query on</param>
/// <param name="token">CancellationToken</param>
/// <returns></returns>
Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default);
} }
} }

View File

@ -7,16 +7,15 @@ namespace SharedLibraryCore.Interfaces
/// <summary> /// <summary>
/// generates and returns a token for the given network id /// generates and returns a token for the given network id
/// </summary> /// </summary>
/// <param name="networkId">network id of the players to generate the token for</param> /// <param name="authInfo">auth information for next token generation</param>
/// <returns>4 character string token</returns> /// <returns>4 character string token</returns>
TokenState GenerateNextToken(long networkId); TokenState GenerateNextToken(ITokenIdentifier authInfo);
/// <summary> /// <summary>
/// authorizes given token /// authorizes given token
/// </summary> /// </summary>
/// <param name="networkId">network id of the client to authorize</param> /// <param name="authInfo">auth information</param>
/// <param name="token">token to authorize</param>
/// <returns>true if token authorized successfully, false otherwise</returns> /// <returns>true if token authorized successfully, false otherwise</returns>
bool AuthorizeToken(long networkId, string token); bool AuthorizeToken(ITokenIdentifier authInfo);
} }
} }

View File

@ -0,0 +1,11 @@

using Data.Models;
namespace SharedLibraryCore.Interfaces;
public interface ITokenIdentifier
{
long NetworkId { get; }
Reference.Game Game { get; set; }
string Token { get; set; }
}

View File

@ -76,7 +76,7 @@ namespace SharedLibraryCore.Database.Models
[NotMapped] [NotMapped]
public virtual int? IPAddress public virtual int? IPAddress
{ {
get => CurrentAlias.IPAddress; get => CurrentAlias?.IPAddress;
set => CurrentAlias.IPAddress = value; set => CurrentAlias.IPAddress = value;
} }
@ -100,7 +100,7 @@ namespace SharedLibraryCore.Database.Models
[NotMapped] public int Score { get; set; } [NotMapped] public int Score { get; set; }
[NotMapped] public bool IsBot => NetworkId == Name.GenerateGuidFromString(); [NotMapped] public bool IsBot => NetworkId == Name.GenerateGuidFromString() || IPAddressString == System.Net.IPAddress.Broadcast.ToString();
[NotMapped] public bool IsZombieClient => IsBot && Name == "Zombie"; [NotMapped] public bool IsZombieClient => IsBot && Name == "Zombie";
@ -682,7 +682,7 @@ namespace SharedLibraryCore.Database.Models
// we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID) // we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID)
var activePenalties = await CurrentServer.Manager.GetPenaltyService() var activePenalties = await CurrentServer.Manager.GetPenaltyService()
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, ipAddress); .GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, GameName, ipAddress);
var banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban); var banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban);
var tempbanPenalty = var tempbanPenalty =
activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan); activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan);

View File

@ -33,7 +33,8 @@ namespace SharedLibraryCore
T6 = 7, T6 = 7,
T7 = 8, T7 = 8,
SHG1 = 9, SHG1 = 9,
CSGO = 10 CSGO = 10,
H1 = 11
} }
// only here for performance // only here for performance
@ -200,7 +201,7 @@ namespace SharedLibraryCore
.ToList(); .ToList();
} }
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken cts) public virtual Task<bool> ProcessUpdatesAsync(CancellationToken token)
{ {
return (Task<bool>)Task.CompletedTask; return (Task<bool>)Task.CompletedTask;
} }

View File

@ -23,25 +23,26 @@ namespace SharedLibraryCore.Services
{ {
public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult> public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult>
{ {
private static readonly Func<DatabaseContext, long, Task<EFClient>> _getUniqueQuery = private static readonly Func<DatabaseContext, long, Reference.Game, Task<EFClient>> GetUniqueQuery =
EF.CompileAsyncQuery((DatabaseContext context, long networkId) => EF.CompileAsyncQuery((DatabaseContext context, long networkId, Reference.Game game) =>
context.Clients context.Clients
.Select(_client => new EFClient .Select(client => new EFClient
{ {
ClientId = _client.ClientId, ClientId = client.ClientId,
AliasLinkId = _client.AliasLinkId, AliasLinkId = client.AliasLinkId,
Level = _client.Level, Level = client.Level,
Connections = _client.Connections, Connections = client.Connections,
FirstConnection = _client.FirstConnection, FirstConnection = client.FirstConnection,
LastConnection = _client.LastConnection, LastConnection = client.LastConnection,
Masked = _client.Masked, Masked = client.Masked,
NetworkId = _client.NetworkId, NetworkId = client.NetworkId,
TotalConnectionTime = _client.TotalConnectionTime, TotalConnectionTime = client.TotalConnectionTime,
AliasLink = _client.AliasLink, AliasLink = client.AliasLink,
Password = _client.Password, Password = client.Password,
PasswordSalt = _client.PasswordSalt PasswordSalt = client.PasswordSalt,
GameName = client.GameName
}) })
.FirstOrDefault(c => c.NetworkId == networkId) .FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
); );
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
@ -178,6 +179,7 @@ namespace SharedLibraryCore.Services
.Select(_client => new EFClient .Select(_client => new EFClient
{ {
ClientId = _client.ClientId, ClientId = _client.ClientId,
GameName = _client.GameName,
AliasLinkId = _client.AliasLinkId, AliasLinkId = _client.AliasLinkId,
Level = _client.Level, Level = _client.Level,
Connections = _client.Connections, Connections = _client.Connections,
@ -234,10 +236,10 @@ namespace SharedLibraryCore.Services
return foundClient.Client; return foundClient.Client;
} }
public virtual async Task<EFClient> GetUnique(long entityAttribute) public virtual async Task<EFClient> GetUnique(long entityAttribute, object altKey = null)
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
return await _getUniqueQuery(context, entityAttribute); return await GetUniqueQuery(context, entityAttribute, (Reference.Game)altKey);
} }
public async Task<EFClient> Update(EFClient temporalClient) public async Task<EFClient> Update(EFClient temporalClient)
@ -284,7 +286,7 @@ namespace SharedLibraryCore.Services
entity.PasswordSalt = temporalClient.PasswordSalt; entity.PasswordSalt = temporalClient.PasswordSalt;
} }
entity.GameName ??= temporalClient.GameName; entity.GameName = temporalClient.GameName;
// update in database // update in database
await context.SaveChangesAsync(); await context.SaveChangesAsync();
@ -757,19 +759,20 @@ namespace SharedLibraryCore.Services
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
return await context.Clients return await context.Clients
.Select(_client => new EFClient .Select(client => new EFClient
{ {
NetworkId = _client.NetworkId, NetworkId = client.NetworkId,
ClientId = _client.ClientId, ClientId = client.ClientId,
CurrentAlias = new EFAlias CurrentAlias = new EFAlias
{ {
Name = _client.CurrentAlias.Name Name = client.CurrentAlias.Name
}, },
Password = _client.Password, Password = client.Password,
PasswordSalt = _client.PasswordSalt, PasswordSalt = client.PasswordSalt,
Level = _client.Level GameName = client.GameName,
Level = client.Level
}) })
.FirstAsync(_client => _client.ClientId == clientId); .FirstAsync(client => client.ClientId == clientId);
} }
public async Task<List<EFClient>> GetPrivilegedClients(bool includeName = true) public async Task<List<EFClient>> GetPrivilegedClients(bool includeName = true)
@ -789,7 +792,8 @@ namespace SharedLibraryCore.Services
PasswordSalt = client.PasswordSalt, PasswordSalt = client.PasswordSalt,
NetworkId = client.NetworkId, NetworkId = client.NetworkId,
LastConnection = client.LastConnection, LastConnection = client.LastConnection,
Masked = client.Masked Masked = client.Masked,
GameName = client.GameName
}; };
return await iqClients.ToListAsync(); return await iqClients.ToListAsync();
@ -858,15 +862,16 @@ namespace SharedLibraryCore.Services
// we want to project our results // we want to project our results
var iqClientProjection = iqClients.OrderByDescending(_client => _client.LastConnection) var iqClientProjection = iqClients.OrderByDescending(_client => _client.LastConnection)
.Select(_client => new PlayerInfo .Select(client => new PlayerInfo
{ {
Name = _client.CurrentAlias.Name, Name = client.CurrentAlias.Name,
LevelInt = (int)_client.Level, LevelInt = (int)client.Level,
LastConnection = _client.LastConnection, LastConnection = client.LastConnection,
ClientId = _client.ClientId, ClientId = client.ClientId,
IPAddress = _client.CurrentAlias.IPAddress.HasValue IPAddress = client.CurrentAlias.IPAddress.HasValue
? _client.CurrentAlias.SearchableIPAddress ? client.CurrentAlias.SearchableIPAddress
: "" : "",
Game = client.GameName
}); });
var clients = await iqClientProjection.ToListAsync(); var clients = await iqClientProjection.ToListAsync();

View File

@ -88,7 +88,7 @@ namespace SharedLibraryCore.Services
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<EFPenalty> GetUnique(long entityProperty) public Task<EFPenalty> GetUnique(long entityProperty, object altKey)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -139,10 +139,10 @@ namespace SharedLibraryCore.Services
LinkedPenalties.Contains(pi.Penalty.Type) && pi.Penalty.Active && LinkedPenalties.Contains(pi.Penalty.Type) && pi.Penalty.Active &&
(pi.Penalty.Expires == null || pi.Penalty.Expires > DateTime.UtcNow); (pi.Penalty.Expires == null || pi.Penalty.Expires > DateTime.UtcNow);
public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int currentAliasId, long networkId, public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int currentAliasId, long networkId, Reference.Game game,
int? ip = null) int? ip = null)
{ {
var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId); var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId, game);
if (penaltiesByIdentifier.Any()) if (penaltiesByIdentifier.Any())
{ {
@ -183,12 +183,12 @@ namespace SharedLibraryCore.Services
return activePenalties.OrderByDescending(p => p.When).ToList(); return activePenalties.OrderByDescending(p => p.When).ToList();
} }
public async Task<List<EFPenalty>> GetActivePenaltiesByIdentifier(int? ip, long networkId) public async Task<List<EFPenalty>> GetActivePenaltiesByIdentifier(int? ip, long networkId, Reference.Game game)
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
var activePenaltiesIds = context.PenaltyIdentifiers.Where(identifier => var activePenaltiesIds = context.PenaltyIdentifiers.Where(identifier =>
identifier.IPv4Address != null && identifier.IPv4Address == ip || identifier.NetworkId == networkId) identifier.IPv4Address != null && identifier.IPv4Address == ip || identifier.NetworkId == networkId && identifier.Penalty.Offender.GameName == game)
.Where(FilterById); .Where(FilterById);
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync(); return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
} }
@ -214,12 +214,12 @@ namespace SharedLibraryCore.Services
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync(); return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
} }
public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, int? ipAddress = null) public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, Reference.Game game, int? ipAddress = null)
{ {
await using var context = _contextFactory.CreateContext(); await using var context = _contextFactory.CreateContext();
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId); var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId, game);
if (activePenalties.Any()) if (activePenalties.Any())
{ {

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.3.23.1</Version> <Version>2022.6.15.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.3.23.1</PackageVersion> <PackageVersion>2022.6.15.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup> </PropertyGroup>

View File

@ -47,6 +47,9 @@ namespace SharedLibraryCore
public static char[] DirectorySeparatorChars = { '\\', '/' }; public static char[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!'; public static char CommandPrefix { get; set; } = '!';
public static string ToStandardFormat(this DateTime? time) => time?.ToString("yyyy-MM-dd H:mm:ss UTC");
public static string ToStandardFormat(this DateTime time) => time.ToString("yyyy-MM-dd H:mm:ss UTC");
public static EFClient IW4MAdminClient(Server server = null) public static EFClient IW4MAdminClient(Server server = null)
{ {
return new EFClient return new EFClient
@ -1178,7 +1181,8 @@ namespace SharedLibraryCore
Meta = client.Meta, Meta = client.Meta,
ReceivedPenalties = client.ReceivedPenalties, ReceivedPenalties = client.ReceivedPenalties,
AdministeredPenalties = client.AdministeredPenalties, AdministeredPenalties = client.AdministeredPenalties,
Active = client.Active Active = client.Active,
GameName = client.GameName
}; };
} }
@ -1261,5 +1265,8 @@ namespace SharedLibraryCore
return allRules[index]; return allRules[index];
} }
public static string MakeAbbreviation(string gameName) => string.Join("",
gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
} }
} }

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using WebfrontCore.Controllers.API.Dtos; using WebfrontCore.Controllers.API.Dtos;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -100,9 +101,16 @@ namespace WebfrontCore.Controllers.API
if (!Authorized) if (!Authorized)
{ {
var tokenData = new TokenIdentifier
{
Game = privilegedClient.GameName,
Token = request.Password,
NetworkId = privilegedClient.NetworkId
};
loginSuccess = loginSuccess =
Manager.TokenAuthenticator.AuthorizeToken(privilegedClient.NetworkId, request.Password) || Manager.TokenAuthenticator.AuthorizeToken(tokenData) ||
(await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(request.Password, (await Task.FromResult(Hashing.Hash(request.Password,
privilegedClient.PasswordSalt)))[0] == privilegedClient.Password; privilegedClient.PasswordSalt)))[0] == privilegedClient.Password;
} }
@ -120,7 +128,7 @@ namespace WebfrontCore.Controllers.API
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity); var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await SignInAsync(claimsPrinciple); await SignInAsync(claimsPrinciple);
Manager.AddEvent(new GameEvent() Manager.AddEvent(new GameEvent
{ {
Origin = privilegedClient, Origin = privilegedClient,
Type = GameEvent.EventType.Login, Type = GameEvent.EventType.Login,
@ -149,7 +157,7 @@ namespace WebfrontCore.Controllers.API
{ {
if (Authorized) if (Authorized)
{ {
Manager.AddEvent(new GameEvent() Manager.AddEvent(new GameEvent
{ {
Origin = Client, Origin = Client,
Type = GameEvent.EventType.Logout, Type = GameEvent.EventType.Logout,

View File

@ -7,7 +7,7 @@ using System;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using SharedLibraryCore.Helpers;
namespace WebfrontCore.Controllers namespace WebfrontCore.Controllers
{ {
@ -19,6 +19,7 @@ namespace WebfrontCore.Controllers
} }
[HttpGet] [HttpGet]
[Obsolete]
public async Task<IActionResult> Login(int clientId, string password) public async Task<IActionResult> Login(int clientId, string password)
{ {
if (clientId == 0 || string.IsNullOrEmpty(password)) if (clientId == 0 || string.IsNullOrEmpty(password))
@ -29,14 +30,23 @@ namespace WebfrontCore.Controllers
try try
{ {
var privilegedClient = await Manager.GetClientService().GetClientForLogin(clientId); var privilegedClient = await Manager.GetClientService().GetClientForLogin(clientId);
bool loginSuccess = false; var loginSuccess = false;
#if DEBUG
loginSuccess = clientId == 1; if (Utilities.IsDevelopment)
#endif {
loginSuccess = clientId == 1;
}
if (!Authorized && !loginSuccess) if (!Authorized && !loginSuccess)
{ {
loginSuccess = Manager.TokenAuthenticator.AuthorizeToken(privilegedClient.NetworkId, password) || loginSuccess = Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
(await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(password, privilegedClient.PasswordSalt)))[0] == privilegedClient.Password; {
NetworkId = privilegedClient.NetworkId,
Game = privilegedClient.GameName,
Token = password
}) ||
(await Task.FromResult(Hashing.Hash(password, privilegedClient.PasswordSalt)))[0] ==
privilegedClient.Password;
} }
if (loginSuccess) if (loginSuccess)
@ -46,21 +56,22 @@ namespace WebfrontCore.Controllers
new Claim(ClaimTypes.NameIdentifier, privilegedClient.Name), new Claim(ClaimTypes.NameIdentifier, privilegedClient.Name),
new Claim(ClaimTypes.Role, privilegedClient.Level.ToString()), new Claim(ClaimTypes.Role, privilegedClient.Level.ToString()),
new Claim(ClaimTypes.Sid, privilegedClient.ClientId.ToString()), new Claim(ClaimTypes.Sid, privilegedClient.ClientId.ToString()),
new Claim(ClaimTypes.PrimarySid, privilegedClient.NetworkId.ToString("X")) new Claim(ClaimTypes.PrimarySid, privilegedClient.NetworkId.ToString("X")),
new Claim(ClaimTypes.PrimaryGroupSid, privilegedClient.GameName.ToString())
}; };
var claimsIdentity = new ClaimsIdentity(claims, "login"); var claimsIdentity = new ClaimsIdentity(claims, "login");
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity); var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await SignInAsync(claimsPrinciple); await SignInAsync(claimsPrinciple);
Manager.AddEvent(new GameEvent() Manager.AddEvent(new GameEvent
{ {
Origin = privilegedClient, Origin = privilegedClient,
Type = GameEvent.EventType.Login, Type = GameEvent.EventType.Login,
Owner = Manager.GetServers().First(), Owner = Manager.GetServers().First(),
Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For") Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString() ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString() : HttpContext.Connection.RemoteIpAddress?.ToString()
}); });
return Ok($"Welcome {privilegedClient.Name}. You are now logged in"); return Ok($"Welcome {privilegedClient.Name}. You are now logged in");
@ -80,14 +91,14 @@ namespace WebfrontCore.Controllers
{ {
if (Authorized) if (Authorized)
{ {
Manager.AddEvent(new GameEvent() Manager.AddEvent(new GameEvent
{ {
Origin = Client, Origin = Client,
Type = GameEvent.EventType.Logout, Type = GameEvent.EventType.Logout,
Owner = Manager.GetServers().First(), Owner = Manager.GetServers().First(),
Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For") Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString() ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString() : HttpContext.Connection.RemoteIpAddress?.ToString()
}); });
} }

View File

@ -10,6 +10,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using WebfrontCore.Permissions; using WebfrontCore.Permissions;
using WebfrontCore.ViewModels; using WebfrontCore.ViewModels;
@ -24,6 +25,7 @@ namespace WebfrontCore.Controllers
private readonly string _unbanCommandName; private readonly string _unbanCommandName;
private readonly string _sayCommandName; private readonly string _sayCommandName;
private readonly string _kickCommandName; private readonly string _kickCommandName;
private readonly string _offlineMessageCommandName;
private readonly string _flagCommandName; private readonly string _flagCommandName;
private readonly string _unflagCommandName; private readonly string _unflagCommandName;
private readonly string _setLevelCommandName; private readonly string _setLevelCommandName;
@ -64,6 +66,9 @@ namespace WebfrontCore.Controllers
case nameof(SetLevelCommand): case nameof(SetLevelCommand):
_setLevelCommandName = cmd.Name; _setLevelCommandName = cmd.Name;
break; break;
case "OfflineMessageCommand":
_offlineMessageCommandName = cmd.Name;
break;
} }
} }
} }
@ -142,7 +147,7 @@ namespace WebfrontCore.Controllers
})); }));
} }
public IActionResult UnbanForm() public IActionResult UnbanForm(long? id)
{ {
var info = new ActionInfo var info = new ActionInfo
{ {
@ -159,6 +164,15 @@ namespace WebfrontCore.Controllers
Action = "UnbanAsync", Action = "UnbanAsync",
ShouldRefresh = true ShouldRefresh = true
}; };
if (id is not null)
{
info.Inputs.Add(new()
{
Name = "targetId",
Value = id.ToString(),
Type = "hidden"
});
}
return View("_ActionForm", info); return View("_ActionForm", info);
} }
@ -204,7 +218,7 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> Login(int clientId, string password) public async Task<IActionResult> Login(int clientId, string password)
{ {
return await Task.FromResult(RedirectToAction("Login", "Account", new {clientId, password})); return await Task.FromResult(RedirectToAction("Login", "Account", new { clientId, password }));
} }
public IActionResult EditForm() public IActionResult EditForm()
@ -261,7 +275,12 @@ namespace WebfrontCore.Controllers
[Authorize] [Authorize]
public string GenerateLoginTokenAsync() public string GenerateLoginTokenAsync()
{ {
var state = Manager.TokenAuthenticator.GenerateNextToken(Client.NetworkId); var state = Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
{
NetworkId = Client.NetworkId,
Game = Client.GameName
});
return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"], return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"],
state.Token, state.Token,
$"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}", $"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}",
@ -318,26 +337,27 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> RecentClientsForm(PaginationRequest request) public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
{ {
ViewBag.First = request.Offset == 0; ViewBag.First = request.Offset == 0;
if (request.Count > 20) if (request.Count > 20)
{ {
request.Count = 20; request.Count = 20;
} }
var clients = await Manager.GetClientService().GetRecentClients(request); var clients = await Manager.GetClientService().GetRecentClients(request);
return request.Offset == 0 return request.Offset == 0
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients) ? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients); : View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
} }
public IActionResult RecentReportsForm() public IActionResult RecentReportsForm()
{ {
var serverInfo = Manager.GetServers().Select(server => var serverInfo = Manager.GetServers().Select(server =>
new ServerInfo new ServerInfo
{ {
Name = server.Hostname, Name = server.Hostname,
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24).ToList() Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24)
.ToList()
}); });
return View("Partials/_Reports", serverInfo); return View("Partials/_Reports", serverInfo);
@ -464,6 +484,105 @@ namespace WebfrontCore.Controllers
})); }));
} }
public IActionResult DismissAlertForm(Guid id)
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss Alert?",
Inputs = new List<InputInfo>
{
new()
{
Name = "alertId",
Type = "hidden",
Value = id.ToString()
}
},
Action = nameof(DismissAlert),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAlert(Guid alertId)
{
AlertManager.MarkAlertAsRead(alertId);
return Json(new[]
{
new CommandResponseInfo
{
Response = "Alert dismissed"
}
});
}
public IActionResult DismissAllAlertsForm()
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss All Alerts?",
Inputs = new List<InputInfo>
{
new()
{
Name = "targetId",
Type = "hidden",
Value = Client.ClientId.ToString()
}
},
Action = nameof(DismissAllAlerts),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAllAlerts(int targetId)
{
AlertManager.MarkAllAlertsAsRead(targetId);
return Json(new[]
{
new CommandResponseInfo
{
Response = "Alerts dismissed"
}
});
}
public IActionResult OfflineMessageForm()
{
var info = new ActionInfo
{
ActionButtonLabel = "Send",
Name = "Compose Message",
Inputs = new List<InputInfo>
{
new()
{
Name = "message",
Label = "Message Content",
},
},
Action = "OfflineMessage",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> OfflineMessage(int targetId, string message)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_offlineMessageCommandName} @{targetId} {message.CapClientName(500)}"
}));
}
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
.Concat(_appConfig.GlobalRules) .Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>())) .Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))

View File

@ -4,6 +4,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Threading.Tasks; using System.Threading.Tasks;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore.Controllers namespace WebfrontCore.Controllers
{ {
@ -11,12 +12,16 @@ namespace WebfrontCore.Controllers
{ {
private readonly IAuditInformationRepository _auditInformationRepository; private readonly IAuditInformationRepository _auditInformationRepository;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IResourceQueryHelper<BanInfoRequest, BanInfo> _banInfoQueryHelper;
private static readonly int DEFAULT_COUNT = 25; private static readonly int DEFAULT_COUNT = 25;
public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository, ITranslationLookup translationLookup) : base(manager) public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository,
ITranslationLookup translationLookup,
IResourceQueryHelper<BanInfoRequest, BanInfo> banInfoQueryHelper) : base(manager)
{ {
_auditInformationRepository = auditInformationRepository; _auditInformationRepository = auditInformationRepository;
_translationLookup = translationLookup; _translationLookup = translationLookup;
_banInfoQueryHelper = banInfoQueryHelper;
} }
[Authorize] [Authorize]
@ -27,7 +32,7 @@ namespace WebfrontCore.Controllers
ViewBag.Title = _translationLookup["WEBFRONT_NAV_AUDIT_LOG"]; ViewBag.Title = _translationLookup["WEBFRONT_NAV_AUDIT_LOG"];
ViewBag.InitialOffset = DEFAULT_COUNT; ViewBag.InitialOffset = DEFAULT_COUNT;
var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest() var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest
{ {
Count = DEFAULT_COUNT Count = DEFAULT_COUNT
}); });
@ -41,5 +46,25 @@ namespace WebfrontCore.Controllers
var auditItems = await _auditInformationRepository.ListAuditInformation(paginationInfo); var auditItems = await _auditInformationRepository.ListAuditInformation(paginationInfo);
return PartialView("_ListAuditLog", auditItems); return PartialView("_ListAuditLog", auditItems);
} }
public async Task<IActionResult> BanManagement([FromQuery] BanInfoRequest request)
{
var results = await _banInfoQueryHelper.QueryResource(request);
ViewBag.ClientName = request.ClientName;
ViewBag.ClientId = request.ClientId;
ViewBag.ClientIP = request.ClientIP;
ViewBag.ClientGuid = request.ClientGuid;
ViewBag.Title = "Ban Management";
return View(results.Results);
}
public async Task<IActionResult> BanManagementList([FromQuery] BanInfoRequest request)
{
var results = await _banInfoQueryHelper.QueryResource(request);
return PartialView("_BanEntries", results.Results);
}
} }
} }

View File

@ -47,7 +47,7 @@ namespace WebfrontCore.Controllers
} }
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId, var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId,
client.CurrentAliasId, client.NetworkId, client.IPAddress); client.CurrentAliasId, client.NetworkId, client.GameName, client.IPAddress);
var persistentMetaTask = new[] var persistentMetaTask = new[]
{ {
@ -88,6 +88,7 @@ namespace WebfrontCore.Controllers
var clientDto = new PlayerInfo var clientDto = new PlayerInfo
{ {
Name = client.Name, Name = client.Name,
Game = client.GameName,
Level = displayLevel, Level = displayLevel,
LevelInt = displayLevelInt, LevelInt = displayLevelInt,
ClientId = client.ClientId, ClientId = client.ClientId,
@ -181,7 +182,8 @@ namespace WebfrontCore.Controllers
Name = admin.Name, Name = admin.Name,
ClientId = admin.ClientId, ClientId = admin.ClientId,
LastConnection = admin.LastConnection, LastConnection = admin.LastConnection,
IsMasked = admin.Masked IsMasked = admin.Masked,
Game = admin.GameName
}); });
} }

View File

@ -1,5 +1,7 @@
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
@ -13,26 +15,43 @@ namespace WebfrontCore.Controllers
{ {
private IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> _queryHelper; private IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> _queryHelper;
private readonly DefaultSettings _defaultConfig; private readonly DefaultSettings _defaultConfig;
private readonly IServerDataViewer _serverDataViewer;
public ClientStatisticsController(IManager manager, public ClientStatisticsController(IManager manager,
IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> queryHelper, IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> queryHelper,
DefaultSettings defaultConfig) : base(manager) DefaultSettings defaultConfig, IServerDataViewer serverDataViewer) : base(manager)
{ {
_queryHelper = queryHelper; _queryHelper = queryHelper;
_defaultConfig = defaultConfig; _defaultConfig = defaultConfig;
_serverDataViewer = serverDataViewer;
} }
[HttpGet("{id:int}/advanced")] [HttpGet("{id:int}/advanced")]
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId) public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId, CancellationToken token = default)
{ {
ViewBag.Config = _defaultConfig.GameStrings; ViewBag.Config = _defaultConfig.GameStrings;
var hitInfo = await _queryHelper.QueryResource(new StatsInfoRequest var hitInfo = (await _queryHelper.QueryResource(new StatsInfoRequest
{ {
ClientId = id, ClientId = id,
ServerEndpoint = serverId ServerEndpoint = serverId
}); }))?.Results?.First();
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo.Results.First()); if (hitInfo is null)
{
return NotFound();
}
var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
long? matchedServerId = null;
if (server != null)
{
matchedServerId = StatManager.GetIdForServer(server);
}
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);
} }
} }
} }

View File

@ -11,6 +11,7 @@ using Stats.Dtos;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -28,10 +29,11 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly StatsConfiguration _config; private readonly StatsConfiguration _config;
private readonly IServerDataViewer _serverDataViewer;
public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery,
MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup, MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory, StatsConfiguration config) : base(manager) IDatabaseContextFactory contextFactory, StatsConfiguration config, IServerDataViewer serverDataViewer) : base(manager)
{ {
_logger = logger; _logger = logger;
_manager = manager; _manager = manager;
@ -39,15 +41,27 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
_translationLookup = translationLookup; _translationLookup = translationLookup;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_config = config; _config = config;
_serverDataViewer = serverDataViewer;
} }
[HttpGet] [HttpGet]
public IActionResult TopPlayers(string serverId = null) public async Task<IActionResult> TopPlayers(string serverId = null, CancellationToken token = default)
{ {
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"]; ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"]; ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
ViewBag.Localization = _translationLookup; ViewBag.Localization = _translationLookup;
ViewBag.SelectedServerId = serverId; ViewBag.SelectedServerId = serverId;
var server = _manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
long? matchedServerId = null;
if (server != null)
{
matchedServerId = StatManager.GetIdForServer(server);
}
ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
ViewBag.ServerId = matchedServerId;
return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers() return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers()
.Select(server => new ServerInfo .Select(server => new ServerInfo

View File

@ -5,6 +5,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using Data.Models;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats.Helpers; using IW4MAdmin.Plugins.Stats.Helpers;
using WebfrontCore.ViewModels; using WebfrontCore.ViewModels;
@ -34,6 +35,7 @@ namespace WebfrontCore.Controllers
ID = s.EndPoint, ID = s.EndPoint,
Port = s.Port, Port = s.Port,
Map = s.CurrentMap.Alias, Map = s.CurrentMap.Alias,
Game = (Reference.Game)s.GameName,
ClientCount = s.Clients.Count(client => client != null), ClientCount = s.Clients.Count(client => client != null),
MaxClients = s.MaxClients, MaxClients = s.MaxClients,
GameType = s.GametypeName, GameType = s.GametypeName,

View File

@ -36,24 +36,26 @@ namespace WebfrontCore.Middleware
/// <param name="gameEvent"></param> /// <param name="gameEvent"></param>
private void OnGameEvent(object sender, GameEvent gameEvent) private void OnGameEvent(object sender, GameEvent gameEvent)
{ {
if (gameEvent.Type == EventType.ChangePermission && if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm)
gameEvent.Extra is EFClient.Permission perm)
{ {
// we want to remove the claims when the client is demoted return;
if (perm < EFClient.Permission.Trusted) }
lock (_privilegedClientIds)
{
switch (perm)
{ {
lock (_privilegedClientIds) // we want to remove the claims when the client is demoted
case < EFClient.Permission.Trusted:
{ {
_privilegedClientIds.RemoveAll(id => id == gameEvent.Target.ClientId); _privilegedClientIds.RemoveAll(id => id == gameEvent.Target.ClientId);
break;
} }
} // and add if promoted
// and add if promoted case > EFClient.Permission.Trusted when !_privilegedClientIds.Contains(gameEvent.Target.ClientId):
else if (perm > EFClient.Permission.Trusted &&
!_privilegedClientIds.Contains(gameEvent.Target.ClientId))
{
lock (_privilegedClientIds)
{ {
_privilegedClientIds.Add(gameEvent.Target.ClientId); _privilegedClientIds.Add(gameEvent.Target.ClientId);
break;
} }
} }
} }
@ -62,10 +64,16 @@ namespace WebfrontCore.Middleware
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
// we want to load the initial list of privileged clients // we want to load the initial list of privileged clients
if (_privilegedClientIds.Count == 0) bool hasAny;
lock (_privilegedClientIds)
{
hasAny = _privilegedClientIds.Any();
}
if (!hasAny)
{ {
var ids = (await _manager.GetClientService().GetPrivilegedClients()) var ids = (await _manager.GetClientService().GetPrivilegedClients())
.Select(_client => _client.ClientId); .Select(client => client.ClientId);
lock (_privilegedClientIds) lock (_privilegedClientIds)
{ {
@ -74,13 +82,19 @@ namespace WebfrontCore.Middleware
} }
// sid stores the clientId // sid stores the clientId
string claimsId = context.User.Claims.FirstOrDefault(_claim => _claim.Type == ClaimTypes.Sid)?.Value; var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
if (!string.IsNullOrEmpty(claimsId)) if (!string.IsNullOrEmpty(claimsId))
{ {
int clientId = int.Parse(claimsId); var clientId = int.Parse(claimsId);
bool hasKey;
lock (_privilegedClientIds)
{
hasKey = _privilegedClientIds.Contains(clientId);
}
// they've been removed // they've been removed
if (!_privilegedClientIds.Contains(clientId) && clientId != 1) if (!hasKey && clientId != 1)
{ {
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
} }

View File

@ -1,6 +1,10 @@
using System.Linq; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models;
using Data.Models.Client; using Data.Models.Client;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore; using SharedLibraryCore;
@ -21,16 +25,26 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
public async Task<ResourceQueryHelperResult<BanInfo>> QueryResource(BanInfoRequest query) public async Task<ResourceQueryHelperResult<BanInfo>> QueryResource(BanInfoRequest query)
{ {
if (query.Count > 30) if (query.Count > 10)
{ {
query.Count = 30; query.Count = 10;
} }
await using var context = _contextFactory.CreateContext(false);
var matchingClients = await context.Clients.Where(client => await using var context = _contextFactory.CreateContext(false);
EF.Functions.ILike(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name, $"%{query.ClientName.Trim()}%"))
.Where(client => client.Level == EFClient.Permission.Banned) var iqMatchingClients = context.Clients.Where(client => client.Level == EFClient.Permission.Banned);
iqMatchingClients = SetupSearchArgs(query, iqMatchingClients);
if (string.IsNullOrEmpty(query.ClientName) && string.IsNullOrEmpty(query.ClientGuid) &&
query.ClientId is null && string.IsNullOrEmpty(query.ClientIP))
{
return new ResourceQueryHelperResult<BanInfo>
{
Results = Enumerable.Empty<BanInfo>()
};
}
var matchingClients = await iqMatchingClients
.OrderByDescending(client => client.LastConnection) .OrderByDescending(client => client.LastConnection)
.Skip(query.Offset) .Skip(query.Offset)
.Take(query.Count) .Take(query.Count)
@ -39,52 +53,118 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
client.CurrentAlias.Name, client.CurrentAlias.Name,
client.NetworkId, client.NetworkId,
client.AliasLinkId, client.AliasLinkId,
client.ClientId client.ClientId,
client.CurrentAlias.IPAddress,
client.GameName
}).ToListAsync(); }).ToListAsync();
var usedIps = await context.Aliases var results = new List<BanInfo>();
.Where(alias => matchingClients.Select(client => client.AliasLinkId).Contains(alias.LinkId)) var matchedClientIds = new List<int?>();
.Where(alias => alias.IPAddress != null) var lateDateTime = DateTime.Now.AddYears(100);
.Select(alias => new { alias.IPAddress, alias.LinkId })
.ToListAsync();
var usedIpsGrouped = usedIps // would prefer not to loop this, but unfortunately due to the data design
.GroupBy(alias => alias.LinkId) // we can't properly group on ip and alias link
.ToDictionary(key => key.Key, value => value.Select(alias => alias.IPAddress).Distinct()); foreach (var matchingClient in matchingClients)
var searchingNetworkIds = matchingClients.Select(client => client.NetworkId);
var searchingIps = usedIpsGrouped.SelectMany(group => group.Value);
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
searchingNetworkIds.Contains(identifier.NetworkId) ||
searchingIps.Contains(identifier.IPv4Address))
.Select(penalty => new
{
penalty.CreatedDateTime,
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense) ? penalty.Penalty.Offense : "Anticheat Detection",
LinkId = penalty.Penalty.Offender.AliasLinkId,
penalty.Penalty.PunisherId
})
.ToListAsync();
var groupedPenalties = matchedPenalties.GroupBy(penalty => penalty.LinkId)
.ToDictionary(key => key.Key, value => value.FirstOrDefault());
var results = matchingClients.Select(client =>
{ {
var matchedPenalty = var usedIps = await context.Aliases
groupedPenalties.ContainsKey(client.AliasLinkId) ? groupedPenalties[client.AliasLinkId] : null; .Where(alias => matchingClient.AliasLinkId == alias.LinkId)
return new BanInfo .Where(alias => alias.IPAddress != null)
.Select(alias => new { alias.IPAddress, alias.LinkId })
.ToListAsync();
var searchingNetworkId = matchingClient.NetworkId;
var searchingIps = usedIps.Select(ip => ip.IPAddress).Distinct();
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
identifier.NetworkId == searchingNetworkId ||
searchingIps.Contains(identifier.IPv4Address))
.Where(identifier => identifier.Penalty.Expires == null || identifier.Penalty.Expires > lateDateTime)
.Select(penalty => new
{
penalty.CreatedDateTime,
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
OffenderName = penalty.Penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense)
? penalty.Penalty.Offense
: "Anticheat Detection",
LinkId = penalty.Penalty.Offender.AliasLinkId,
penalty.Penalty.OffenderId,
penalty.Penalty.PunisherId,
penalty.IPv4Address,
penalty.Penalty.Offender.NetworkId
})
.ToListAsync();
if (!matchedPenalties.Any())
{ {
DateTime = matchedPenalty?.CreatedDateTime, var linkIds = (await context.Aliases
OffenderName = client.Name.StripColors(), .Where(alias => alias.IPAddress != null && searchingIps.Contains(alias.IPAddress))
OffenderId = client.ClientId, .Select(alias => alias.LinkId)
PunisherName = matchedPenalty?.PunisherName.StripColors(), .ToListAsync()).Distinct();
PunisherId = matchedPenalty?.PunisherId,
Offense = matchedPenalty?.Offense matchedPenalties = await context.Penalties.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Ban)
}; .Where(penalty => penalty.Expires == null || penalty.Expires > lateDateTime)
}).ToList(); .Where(penalty => penalty.LinkId != null && linkIds.Contains(penalty.LinkId.Value))
.OrderByDescending(penalty => penalty.When)
.Select(penalty => new
{
CreatedDateTime = penalty.When,
PunisherName = penalty.Punisher.CurrentAlias.Name,
OffenderName = penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.AutomatedOffense)
? penalty.Offense
: "Anticheat Detection",
LinkId = penalty.Offender.AliasLinkId,
penalty.OffenderId,
penalty.PunisherId,
IPv4Address = penalty.Offender.CurrentAlias.IPAddress,
penalty.Offender.NetworkId
}).ToListAsync();
}
var allPenalties = matchedPenalties.Select(penalty => new PenaltyInfo
{
DateTime = penalty.CreatedDateTime,
Offense = penalty.Offense,
PunisherInfo = new RelatedClientInfo
{
ClientName = penalty.PunisherName.StripColors(),
ClientId = penalty.PunisherId,
},
OffenderInfo = new RelatedClientInfo
{
ClientName = penalty.OffenderName.StripColors(),
ClientId = penalty.OffenderId,
IPAddress = penalty.IPv4Address,
NetworkId = penalty.NetworkId
}
}).ToList();
if (matchedClientIds.Contains(matchingClient.ClientId))
{
continue;
}
matchedClientIds.Add(matchingClient.ClientId);
var relatedEntities =
allPenalties.Where(penalty => penalty.OffenderInfo.ClientId != matchingClient.ClientId);
matchedClientIds.AddRange(relatedEntities.Select(client => client.OffenderInfo.ClientId));
results.Add(new BanInfo
{
ClientName = matchingClient.Name.StripColors(),
ClientId = matchingClient.ClientId,
NetworkId = matchingClient.NetworkId,
IPAddress = matchingClient.IPAddress,
Game = matchingClient.GameName,
AssociatedPenalties = relatedEntities,
AttachedPenalty = allPenalties.FirstOrDefault(penalty =>
penalty.OffenderInfo.ClientId == matchingClient.ClientId)
});
}
return new ResourceQueryHelperResult<BanInfo> return new ResourceQueryHelperResult<BanInfo>
{ {
@ -93,4 +173,61 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
Results = results Results = results
}; };
} }
private IQueryable<EFClient> SetupSearchArgs(BanInfoRequest query, IQueryable<EFClient> source)
{
if (!string.IsNullOrEmpty(query.ClientName))
{
var nameToSearch = query.ClientName.Trim().ToLower();
source = source.Where(client =>
EF.Functions.Like(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name.ToLower(),
$"%{nameToSearch}%"));
}
if (!string.IsNullOrEmpty(query.ClientGuid))
{
long? parsedGuid = null;
if (!long.TryParse(query.ClientGuid, NumberStyles.HexNumber, null, out var guid))
{
if (!long.TryParse(query.ClientGuid, out var guid2))
{
}
else
{
parsedGuid = guid2;
}
}
else
{
parsedGuid = guid;
}
if (parsedGuid is not null)
{
source = source.Where(client => client.NetworkId == parsedGuid);
}
}
if (query.ClientId is not null)
{
source = source.Where(client => client.ClientId == query.ClientId);
}
if (string.IsNullOrEmpty(query.ClientIP))
{
return source;
}
var parsedIp = query.ClientIP.ConvertToIP();
if (parsedIp is not null)
{
source = source.Where(client => client.CurrentAlias.IPAddress == parsedIp);
}
else
{
query.ClientIP = null;
}
return source;
}
} }

View File

@ -1,12 +1,35 @@
using System; using System;
using System.Collections.Generic;
using Data.Models;
namespace WebfrontCore.QueryHelpers.Models;
public class BanInfo public class BanInfo
{ {
public string OffenderName { get; set; } public string ClientName { get; set; }
public int OffenderId { get; set; } public int ClientId { get; set; }
public string PunisherName { get; set; } public int? IPAddress { get; set; }
public int? PunisherId { get; set; } public long NetworkId { get; set; }
public Reference.Game Game { get; set; }
public PenaltyInfo AttachedPenalty { get; set; }
public IEnumerable<PenaltyInfo> AssociatedPenalties { get; set; }
}
public class PenaltyInfo
{
public RelatedClientInfo OffenderInfo { get; set; }
public RelatedClientInfo PunisherInfo { get; set; }
public string Offense { get; set; } public string Offense { get; set; }
public DateTime? DateTime { get; set; } public DateTime? DateTime { get; set; }
public long? TimeStamp => DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null;
public long? TimeStamp =>
DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null;
}
public class RelatedClientInfo
{
public string ClientName { get; set; }
public int? ClientId { get; set; }
public int? IPAddress { get; set; }
public long? NetworkId { get; set; }
} }

View File

@ -5,4 +5,7 @@ namespace WebfrontCore.QueryHelpers.Models;
public class BanInfoRequest : PaginationRequest public class BanInfoRequest : PaginationRequest
{ {
public string ClientName { get; set; } public string ClientName { get; set; }
public string ClientGuid { get; set; }
public int? ClientId { get; set; }
public string ClientIP { get; set; }
} }

View File

@ -4,11 +4,9 @@ using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using Data.Models;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using Microsoft.AspNetCore.Hosting.Server;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
@ -72,6 +70,7 @@ namespace WebfrontCore.ViewComponents
ID = server.EndPoint, ID = server.EndPoint,
Port = server.Port, Port = server.Port,
Map = server.CurrentMap.Alias, Map = server.CurrentMap.Alias,
Game = (Reference.Game)server.GameName,
ClientCount = server.Clients.Count(client => client != null), ClientCount = server.Clients.Count(client => client != null),
MaxClients = server.MaxClients, MaxClients = server.MaxClients,
GameType = server.GametypeName, GameType = server.GametypeName,

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Html;
namespace WebfrontCore.ViewModels; namespace WebfrontCore.ViewModels;
@ -19,7 +20,7 @@ public class TableInfo
public class RowDefinition public class RowDefinition
{ {
public List<string> Datum { get; } = new(); public List<ColumnTypeDefinition> Datum { get; } = new();
} }
public class ColumnDefinition public class ColumnDefinition
@ -28,6 +29,23 @@ public class ColumnDefinition
public string ColumnSpan { get; set; } public string ColumnSpan { get; set; }
} }
public enum ColumnType
{
Text,
Link,
Icon,
Button
}
public class ColumnTypeDefinition
{
public ColumnType Type { get; set; }
public string Value { get; set; }
public string Data { get; set; }
public IHtmlContent Template { get; set; }
public int Id { get; set; }
}
public static class TableInfoExtensions public static class TableInfoExtensions
{ {
public static TableInfo WithColumns(this TableInfo info, IEnumerable<string> columns) public static TableInfo WithColumns(this TableInfo info, IEnumerable<string> columns)
@ -42,6 +60,16 @@ public static class TableInfoExtensions
public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source, public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source,
Func<T, IEnumerable<string>> selector) Func<T, IEnumerable<string>> selector)
{
return WithRows(info, source, (outer) => selector(outer).Select(item => new ColumnTypeDefinition
{
Value = item,
Type = ColumnType.Text
}));
}
public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source,
Func<T, IEnumerable<ColumnTypeDefinition>> selector)
{ {
info.Rows.AddRange(source.Select(row => info.Rows.AddRange(source.Select(row =>
{ {

View File

@ -4,7 +4,7 @@
Layout = null; Layout = null;
} }
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5> <h5 class="modal-title mb-10">@Model.Name.Titleize()</h5>
@if (Model.Inputs.Any()) @if (Model.Inputs.Any(input => input.Type != "hidden"))
{ {
<hr class="mb-10"/> <hr class="mb-10"/>
} }
@ -55,12 +55,12 @@
<input type="@inputType" name="@input.Name" value="@value" hidden="hidden"> <input type="@inputType" name="@input.Name" value="@value" hidden="hidden">
} }
} }
@if (Model.Inputs.Any()) @if (Model.Inputs.Any(input => input.Type != "hidden"))
{ {
<hr class="mb-10"/> <hr class="mb-10"/>
} }
<div class="ml-auto"> <div class="ml-auto">
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button> <button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
<a href="#" class="btn mr-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a> <a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
</div> </div>
</form> </form>

View File

@ -0,0 +1,66 @@
@model IEnumerable<WebfrontCore.QueryHelpers.Models.BanInfo>
<div class="content mt-0">
<h2 class="content-title mt-20 mb-10">@ViewBag.Title</h2>
@if (!Model.Any())
{
<div class="text-muted mb-10">Search for records...</div>
}
<form method="get" class="mt-10">
<div class="d-flex flex-column flex-md-row">
<div class="input-group">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="Client Name">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
<div class="input-group mr-md-5 ml-md-10 mt-10 mb-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="Client GUID">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
<div class="input-group mr-md-10 ml-md-5 mb-10 mt-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="Client IP">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
<div class="input-group">
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="Client Id">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
</div>
</form>
<div id="ban_entry_list">
<partial name="_BanEntries" for="@Model"/>
</div>
@if (Model.Any())
{
<div class="w-full text-center">
<i id="loaderLoad" class="oi oi-chevron-bottom mt-10 loader-load-more text-primary m-auto" aria-hidden="true"></i>
</div>
}
</div>
@section scripts {
<script>
initLoader('/Admin/BanManagementList', '#ban_entry_list', 10, 10, [{ 'name': 'clientIP', 'value': () => $('#clientIPInput').val() },
{ 'name': 'clientGuid', 'value': () => $('#clientGuidInput').val() },
{ 'name': 'clientName', 'value': () => $('#clientNameInput').val() },
{ 'name': 'clientId', 'value': () => $('#clientIdInput').val() }]);
</script>
}

View File

@ -0,0 +1,65 @@
@model IEnumerable<WebfrontCore.QueryHelpers.Models.BanInfo>
@foreach (var ban in Model)
{
if (ban.AttachedPenalty is null && !ban.AssociatedPenalties.Any())
{
continue;
}
<div class="card p-10 m-0 mt-15 mb-15">
<div class="d-flex flex-row flex-wrap">
<div class="d-flex p-15 mr-md-10 w-full w-md-200 bg-very-dark-dm bg-light-ex-lm rounded">
<div class="align-self-center ">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="font-size-18 no-decoration">@ban.ClientName</a>
<br/>
<div class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
<has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@ban.NetworkId.ToString("X")</div>
</has-permission>
<has-permission entity="ClientIPAddress" required-permission="Read">
<div class="text-muted">@ban.IPAddress.ConvertIPtoString()</div>
</has-permission>
<br/>
@if (ban.AttachedPenalty is not null)
{
<div class="text-muted font-weight-light">@ban.AttachedPenalty.Offense.CapClientName(30)</div>
<div class="text-danger font-weight-light">@ban.AttachedPenalty.DateTime.ToStandardFormat()</div>
<div class="btn profile-action w-100" data-action="unban" data-action-id="@ban.ClientId">Unban</div>
}
else
{
<div class="align-self-end text-muted font-weight-light">
<span class="oi oi-warning font-size-12"></span>
<span>Link-Only Ban</span>
</div>
}
</div>
</div>
@foreach (var associatedEntity in ban.AssociatedPenalties)
{
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10">
<div data-toggle="tooltip" data-title="Linked via shared IP" class="d-flex">
<i class="oi oi-link-intact align-self-center"></i>
<div class="text-truncate ml-5 mr-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="font-size-18 no-decoration">@associatedEntity.OffenderInfo.ClientName</a>
</div>
</div>
<has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@associatedEntity.OffenderInfo.NetworkId?.ToString("X")</div>
</has-permission>
<has-permission entity="ClientIPAddress" required-permission="Read">
<div class="text-muted">@associatedEntity.OffenderInfo.IPAddress.ConvertIPtoString()</div>
</has-permission>
<br/>
<div class="text-muted font-weight-light">@associatedEntity.Offense.CapClientName(30)</div>
<div class="text-danger font-weight-light">@associatedEntity.DateTime.ToStandardFormat()</div>
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">Unban</div>
<div class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
</div>
}
</div>
</div>
}

View File

@ -5,10 +5,11 @@
@foreach (var key in Model.Keys) @foreach (var key in Model.Keys)
{ {
<table class="table mb-20"> <table class="table mb-20" style="table-layout:fixed;">
<thead> <thead>
<tr class="level-bgcolor-@((int)key)"> <tr class="level-bgcolor-@((int)key)">
<th class="text-light">@key.ToLocalizedLevelName()</th> <th class="text-light">@key.ToLocalizedLevelName()</th>
<th>Game</th>
<th class="text-right font-weight-bold">Last Connected</th> <th class="text-right font-weight-bold">Last Connected</th>
</tr> </tr>
</thead> </thead>
@ -33,6 +34,9 @@
<color-code value="@client.Name"></color-code> <color-code value="@client.Name"></color-code>
</a> </a>
</td> </td>
<td>
<div class="badge">@ViewBag.Localization[$"GAME_{client.Game}"]</div>
</td>
<td class="text-right"> <td class="text-right">
@client.LastConnection.HumanizeForCurrentCulture() @client.LastConnection.HumanizeForCurrentCulture()
</td> </td>

View File

@ -32,7 +32,7 @@
} }
<div class="content row mt-20"> <div class="content row mt-20">
<div class="col-12 col-lg-9 col-xl-10"> <div class="col-12 col-lg-9">
@if (Model.ActivePenalty != null) @if (Model.ActivePenalty != null)
{ {
<has-permission entity="ClientLevel" required-permission="Read"> <has-permission entity="ClientLevel" required-permission="Read">
@ -58,7 +58,8 @@
</has-permission> </has-permission>
} }
<h2 class="content-title mb-10">Player Profile</h2> <h2 class="content-title mb-0">Player Profile</h2>
<div class="font-size-12 text-muted">@ViewBag.Localization[$"GAME_{Model.Game}"]</div>
<div id="profile_wrapper" class="mb-10 mt-10"> <div id="profile_wrapper" class="mb-10 mt-10">
@ -200,7 +201,7 @@
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<hr class="mr-5 ml-5"/> <hr class="mr-5 ml-5"/>
<!-- meta info block --> <!-- meta info block -->
@ -278,6 +279,18 @@
EntityId = Model.ClientId EntityId = Model.ClientId
}); });
} }
if (ViewBag.Authorized)
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Message",
IsButton = true,
Reference = "OfflineMessage",
Icon = "oi oi-envelope-closed",
EntityId = Model.ClientId
});
}
menuItems.Items.Add(new SideContextMenuItem menuItems.Items.Add(new SideContextMenuItem
{ {
@ -299,7 +312,7 @@
EntityId = Model.ClientId EntityId = Model.ClientId
}); });
} }
if (Model.LevelInt < (int)ViewBag.User.Level && Model.Online) if (Model.LevelInt < (int)ViewBag.User.Level && Model.Online)
{ {
menuItems.Items.Add(new SideContextMenuItem menuItems.Items.Add(new SideContextMenuItem
@ -335,6 +348,7 @@
EntityId = Model.ClientId EntityId = Model.ClientId
}); });
} }
} }
<partial name="_SideContextMenu" for="@menuItems"></partial> <partial name="_SideContextMenu" for="@menuItems"></partial>

View File

@ -232,7 +232,7 @@
<div class="content row mt-20"> <div class="content row mt-20">
<!-- main content --> <!-- main content -->
<div class="col-12 col-lg-9 col-xl-10 mt-0"> <div class="col-12 col-lg-9 mt-0">
<h2 class="content-title mb-0">Player Stats</h2> <h2 class="content-title mb-0">Player Stats</h2>
<span class="text-muted"> <span class="text-muted">
<color-code value="@(Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code> <color-code value="@(Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
@ -256,7 +256,7 @@
{ {
if (Model.Ranking > 0) if (Model.Ranking > 0)
{ {
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED"] as string).FormatExt(Model.Ranking))</div> <div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED_V2"] as string).FormatExt(Model.Ranking?.ToString("#,##0"), Model.TotalRankedClients.ToString("#,##0")))</div>
} }
else else

View File

@ -2,10 +2,11 @@
@using WebfrontCore.ViewModels @using WebfrontCore.ViewModels
<div class="content mt-20 row"> <div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10 mt-0"> <div class="col-12 col-lg-9 mt-0">
<h2 class="content-title mb-0">Top Players</h2> <h2 class="content-title mb-0">Top Players</h2>
<span class="text-muted"> <span class="text-muted">
<color-code value="@(Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code> <color-code value="@(Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
&mdash; <span class="text-primary">@ViewBag.TotalRankedClients.ToString("#,##0")</span> Ranked Players
</span> </span>
<div id="topPlayersContainer"> <div id="topPlayersContainer">
@ -42,6 +43,7 @@
{ {
<environment include="Development"> <environment include="Development">
<script type="text/javascript" src="~/js/stats.js"></script> <script type="text/javascript" src="~/js/stats.js"></script>
<script type="text/javascript" src="~/lib/canvas.js/canvasjs.js"></script>
</environment> </environment>
<script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25);</script> <script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25, 25, [{ 'name': 'serverId', 'value' : () => @(ViewBag.ServerId ?? 0) }]);</script>
} }

View File

@ -11,7 +11,7 @@
} }
} }
<div class="content mt-20 row"> <div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10"> <div class="col-12 col-lg-9">
<h2 class="content-title mb-0">Server Overview</h2> <h2 class="content-title mb-0">Server Overview</h2>
@if (Model.Game.HasValue) @if (Model.Game.HasValue)
{ {

View File

@ -13,7 +13,7 @@
</style> </style>
<div class="content mt-20 row"> <div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10"> <div class="col-12 col-lg-9">
<h2 class="content-title mb-0">Live Radar</h2> <h2 class="content-title mb-0">Live Radar</h2>
<div class="text-muted mb-15"> <div class="text-muted mb-15">
<color-code value="@((Model.FirstOrDefault(server => server.Endpoint == ViewBag.SelectedServerId) ?? Model.First()).Name)"></color-code> <color-code value="@((Model.FirstOrDefault(server => server.Endpoint == ViewBag.SelectedServerId) ?? Model.First()).Name)"></color-code>

View File

@ -5,7 +5,7 @@
} }
<div class="content mt-20 row"> <div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10"> <div class="col-12 col-lg-9">
@if (Model is not null) @if (Model is not null)
{ {
<div class=" scoreboard-container" data-server-id="@ViewBag.SelectedServerId"> <div class=" scoreboard-container" data-server-id="@ViewBag.SelectedServerId">

View File

@ -19,16 +19,17 @@
} }
<div class="card mt-20 mb-20 ml-0 mr-0 p-0"> <div class="card mt-20 mb-20 ml-0 mr-0 p-0">
<div class="p-5 pl-10 pr-10 bg-primary rounded-top d-flex flex-column flex-md-row flex-wrap justify-content-between text-light" id="server_header_@Model.ID"> <div class="p-5 pl-10 pr-10 bg-primary rounded-top d-flex flex-column flex-lg-row flex-wrap justify-content-between text-light" id="server_header_@Model.ID">
<div class="d-flex align-self-center flex-column-reverse flex-md-row"> <!-- first column -->
<div class="ml-5 mr-5 text-center"> <div class="d-flex align-self-center flex-column-reverse flex-lg-row col-12 col-lg-6">
<div class="ml-5 mr-5 text-center text-lg-left">
<color-code value="@Model.Name"></color-code> <color-code value="@Model.Name"></color-code>
<div class="server-header-ip-address font-weight-light" style="display:none">@(Model.ExternalIPAddress):@(Model.Port)</div>
</div> </div>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<!-- connect button --> <!-- connect button -->
<a href="@Model.ConnectProtocolUrl" class="text-light align-self-center" title="@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HOME_JOIN_DESC"]"> <a href="@Model.ConnectProtocolUrl" class="text-light align-self-center server-join-button" title="@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HOME_JOIN_DESC"]">
<i class="oi oi-play-circle ml-5 mr-5"></i> <i class="oi oi-play-circle ml-5 mr-5"></i>
<span class="server-header-ip-address" style="display:none;">@(Model.ExternalIPAddress):@(Model.Port)</span>
</a> </a>
<has-permission entity="AdminMenu" required-permission="Update"> <has-permission entity="AdminMenu" required-permission="Update">
<!-- send message button --> <!-- send message button -->
@ -41,17 +42,21 @@
class="text-light align-self-center"> class="text-light align-self-center">
<i class="oi oi-spreadsheet ml-5 mr-5"></i> <i class="oi oi-spreadsheet ml-5 mr-5"></i>
</a> </a>
<span class="ml-5 mr-5 text-light badge font-weight-light" data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{Model.Game}"]">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{Model.Game}"])</span>
</div> </div>
</div> </div>
<div class="align-self-center"> <!-- second column -->
<div class="col-12 align-self-center text-center text-lg-left col-lg-4">
<span>@Model.Map</span> <span>@Model.Map</span>
@if (!string.IsNullOrEmpty(Model.GameType) && Model.GameType.Length > 1) @if (!string.IsNullOrEmpty(Model.GameType) && Model.GameType.Length > 1)
{ {
<span>&ndash;</span> <span>&ndash;</span>
<span>@Model.GameType</span> <span>@Model.GameType</span>
} }
</div> </div>
<div class="align-self-center d-flex flex-column flex-md-row"> <!-- third column -->
<div class="align-self-center d-flex flex-column flex-lg-row col-12 col-lg-2 justify-content-end">
@if (Model.LobbyZScore != null) @if (Model.LobbyZScore != null)
{ {
<div data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_HOME_RATING_DESC"]" class="cursor-help d-flex flex-row-reverse flex-md-row justify-content-center"> <div data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_HOME_RATING_DESC"]" class="cursor-help d-flex flex-row-reverse flex-md-row justify-content-center">

View File

@ -55,10 +55,10 @@
} }
start++; start++;
<div class="profile-meta-entry loader-data-time" data-time="@meta.When.ToFileTimeUtc()" onclick="$('#metaContextDateToggle@(start)').show()"> <div class="profile-meta-entry loader-data-time" data-time="@meta.When.ToFileTimeUtc()" onclick="$('#metaContextDateToggle@(meta.When.ToFileTimeUtc())').show()">
<partial name="~/Views/Client/Profile/Meta/_@(meta.GetType().Name).cshtml" model="meta"/> <partial name="~/Views/Client/Profile/Meta/_@(meta.GetType().Name).cshtml" model="meta"/>
<div style="display:none" id="metaContextDateToggle@(start)"> <div style="display:none" id="metaContextDateToggle@(meta.When.ToFileTimeUtc())">
Event occured at <span class="text-light">@meta.When.ToString()</span> Event occured at <span class="text-light">@meta.When.ToStandardFormat()</span>
</div> </div>
</div> </div>
} }

View File

@ -0,0 +1,68 @@
@using SharedLibraryCore.Alerts
@using Humanizer
@model IEnumerable<SharedLibraryCore.Alerts.Alert.AlertState>
@{
Layout = null;
}
<div class="dropdown with-arrow" data-toggle="dropdown" id="alert-toggle" aria-haspopup="true" aria-expanded="false">
<div data-toggle="tooltip" data-title="@(Model.Any() ? "View Alerts" : "No Alerts")" data-placement="bottom">
<i class="oi oi-bell mt-5"></i>
</div>
@if (Model.Any())
{
<div class="position-absolute bg-danger rounded-circle ml-10" style="width: 0.5em;height: 0.5em;top: 0;"></div>
<div class="dropdown-menu dropdown-menu-right w-250 w-md-400" aria-labelledby="alert-toggle">
<div class="d-flex">
<h6 class="dropdown-header">@ViewBag.Alerts.Count Alerts</h6>
<i class="oi oi-circle-x font-size-12 text-danger align-self-center profile-action" data-action="DismissAllAlerts" data-action-id="@ViewBag.User.ClientId"></i>
</div>
<div class="dropdown-divider"></div>
<div style="max-height: 50vh; overflow: scroll" >
@foreach (var alert in Model)
{
<div class="d-flex p-5 pl-10 pr-10">
<div class="align-self-center">
@if (alert.Category == Alert.AlertCategory.Error)
{
<i class="oi oi-warning text-danger font-size-12 mr-5"></i>
}
@if (alert.Category == Alert.AlertCategory.Warning)
{
<i class="oi oi-warning text-secondary font-size-12 mr-5"></i>
}
@if (alert.Category == Alert.AlertCategory.Information)
{
<i class="oi oi-circle-check font-size-12 mr-5 text-primary"></i>
}
@if (alert.Category == Alert.AlertCategory.Message)
{
<i class="oi oi-envelope-closed font-size-12 mr-5 text-primary"></i>
}
</div>
<div class="font-size-12 p-5">
<span class="text-force-break">@alert.Message</span>
<div class="text-muted d-flex">
<span>@alert.OccuredAt.Humanize()</span>
@if (!string.IsNullOrEmpty(alert.Source))
{
<span class="ml-5 mr-5">&#8226;</span>
@if (alert.SourceId is null)
{
<div class="text-white font-weight-light">@alert.Source.StripColors()</div>
}
else
{
<a asp-controller="Client" asp-action="Profile" asp-route-id="@alert.SourceId" class="no-decoration">@alert.Source</a>
}
}
</div>
</div>
<div class="ml-auto">
<i class="oi oi-circle-x font-size-12 ml-5 align-self-center profile-action" data-action="DismissAlert" data-action-id="@alert.AlertId"></i>
</div>
</div>
}
</div>
</div>
}
</div>

View File

@ -1,4 +1,5 @@
@model WebfrontCore.ViewModels.TableInfo @using WebfrontCore.ViewModels
@model WebfrontCore.ViewModels.TableInfo
@{ @{
Layout = null; Layout = null;
} }
@ -17,7 +18,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@{ var start = 0;} @{ var start = 0; }
@if (!Model.Rows.Any()) @if (!Model.Rows.Any())
{ {
<!-- desktop --> <!-- desktop -->
@ -35,15 +36,40 @@
@foreach (var row in Model.Rows) @foreach (var row in Model.Rows)
{ {
<!-- desktop --> <!-- desktop -->
<tr class="bg-dark-dm bg-light-lm @(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row-lg": "d-none d-lg-table-row")"> <tr class="bg-dark-dm bg-light-lm @(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row-lg" : "d-none d-lg-table-row")">
@for (var i = 0; i < Model.Columns.Count; i++) @for (var i = 0; i < Model.Columns.Count; i++)
{ {
<td>@row.Datum[i]</td> var data = row.Datum[i];
<td>
@if (data.Template is null)
{
if (data.Type == ColumnType.Text)
{
<span>@data.Value</span>
}
if (data.Type == ColumnType.Link)
{
<a href="@data.Data" class="no-decoration">@data.Value</a>
}
if (data.Type == ColumnType.Icon)
{
<span class="oi @data.Value profile-action" data-action="@data.Data" data-action-id="@data.Id"></span>
}
if (data.Type == ColumnType.Button)
{
<div class="btn profile-action" data-action="@data.Data" data-action-id="@data.Id">@data.Value</div>
}
}
else
{
@data.Template
}
</td>
} }
</tr> </tr>
<!-- mobile --> <!-- mobile -->
<tr class="@(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row": "d-flex d-table-row d-lg-none")"> <tr class="@(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row" : "d-flex d-table-row d-lg-none")">
<td class="bg-primary text-light text-right w-125"> <td class="bg-primary text-light text-right w-125">
@foreach (var column in Model.Columns) @foreach (var column in Model.Columns)
{ {
@ -53,7 +79,21 @@
<td class="bg-dark-dm bg-light-lm flex-fill w-200"> <td class="bg-dark-dm bg-light-lm flex-fill w-200">
@for (var i = 0; i < Model.Columns.Count; i++) @for (var i = 0; i < Model.Columns.Count; i++)
{ {
<div class="mt-5 mb-5 text-truncate" style="min-width:0">@row.Datum[i]</div> var data = row.Datum[i];
<div class="mt-5 mb-5 text-truncate" style="min-width:0">
@if (data.Type == ColumnType.Text)
{
<span>@data.Value</span>
}
@if (data.Type == ColumnType.Link)
{
<a href="@data.Data">@data.Value</a>
}
@if (data.Type == ColumnType.Icon)
{
<span class="oi @data.Value profile-action" data-action="@data.Data" data-action-id="@data.Id"></span>
}
</div>
} }
</td> </td>
</tr> </tr>

View File

@ -119,11 +119,13 @@
</div> </div>
<div class="d-flex ml-auto"> <div class="d-flex ml-auto">
<div class="align-self-center">
@await Html.PartialAsync("Partials/_Notifications", (object)ViewBag.Alerts)
</div>
<div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom"> <div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom">
<i class="oi oi-moon"></i> <i class="oi oi-moon"></i>
</div> </div>
<div class="d-none d-md-block "> <div class="d-none d-md-block ">
<partial name="_SearchResourceForm"/> <partial name="_SearchResourceForm"/>
</div> </div>
<div class="d-flex d-lg-none"> <div class="d-flex d-lg-none">

View File

@ -115,32 +115,38 @@
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_CONSOLE"]</span> <span class="name">@ViewBag.Localization["WEBFRONT_NAV_CONSOLE"]</span>
</a> </a>
</has-permission> </has-permission>
@if (ViewBag.User.Level >= EFClient.Permission.Owner) <has-permission entity="Penalty" required-permission="Read"></has-permission>
{ <a asp-controller="Admin" asp-action="BanManagement" class="sidebar-link">
<a asp-controller="Configuration" asp-action="Edit" class="sidebar-link"> <i class="oi oi-ban mr-5"></i>
<i class="oi oi-cog mr-5"></i> <span class="name">Ban Management</span>
<span class="name">Configuration</span> </a>
</a> </has-permission>
} @if (ViewBag.User.Level >= EFClient.Permission.Owner)
<has-permission entity="AuditPage" required-permission="Read"> {
<a asp-controller="Admin" asp-action="AuditLog" class="sidebar-link"> <a asp-controller="Configuration" asp-action="Edit" class="sidebar-link">
<i class="oi oi-book mr-5"></i> <i class="oi oi-cog mr-5"></i>
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_AUDIT_LOG"]</span> <span class="name">Configuration</span>
</a> </a>
</has-permission> }
<has-permission entity="RecentPlayersPage" required-permission="Read"> <has-permission entity="AuditPage" required-permission="Read">
<a class="sidebar-link profile-action" href="#actionModal" data-action="RecentClients" title="@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]"> <a asp-controller="Admin" asp-action="AuditLog" class="sidebar-link">
<i class="oi oi-timer mr-5"></i> <i class="oi oi-book mr-5"></i>
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]</span> <span class="name">@ViewBag.Localization["WEBFRONT_NAV_AUDIT_LOG"]</span>
</a> </a>
</has-permission> </has-permission>
<has-permission entity="RecentPlayersPage" required-permission="Read">
<a class="sidebar-link profile-action" href="#actionModal" data-action="RecentClients" title="@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]">
<i class="oi oi-timer mr-5"></i>
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]</span>
</a>
</has-permission>
@if (ViewBag.Authorized)
{
<a class="sidebar-link profile-action" href="#actionModal" data-action="GenerateLoginToken" data-response-duration="30000" title="@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]"> <a class="sidebar-link profile-action" href="#actionModal" data-action="GenerateLoginToken" data-response-duration="30000" title="@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]">
<i class="oi oi-key mr-5"></i> <i class="oi oi-key mr-5"></i>
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]</span> <span class="name">@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]</span>
</a> </a>
</has-permission>
@if (ViewBag.Authorized)
{
<a asp-controller="Account" asp-action="Logout" class="sidebar-link"> <a asp-controller="Account" asp-action="Logout" class="sidebar-link">
<i class="oi oi-account-logout mr-5"></i> <i class="oi oi-account-logout mr-5"></i>
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_LOGOUT"]</span> <span class="name">@ViewBag.Localization["WEBFRONT_NAV_LOGOUT"]</span>

Some files were not shown because too many files have changed in this diff Show More