update schema to support unique guid + game combinations

This commit is contained in:
RaidMax 2022-06-15 19:37:34 -05:00
parent deeb1dea87
commit 8ae6561f4e
46 changed files with 5449 additions and 181 deletions

View File

@ -632,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()

View File

@ -75,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)
@ -118,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()))
{ {
@ -449,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;
} }
@ -517,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);
} }
@ -763,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)
{ {
@ -980,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
{ {
@ -1530,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

@ -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

@ -314,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

@ -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 =>
{ {

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);
}); });

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);
}); });

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);
}); });

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

@ -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.6.9.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.6.9.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.6.9.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.6.9.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

@ -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

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.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.6.9.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.15.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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;
@ -25,26 +24,32 @@ namespace SharedLibraryCore
/// <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; 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
@ -59,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,
@ -67,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)
{ {
@ -79,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
}); });
@ -99,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),
@ -107,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);
} }
} }
@ -134,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();
@ -153,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

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

@ -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

@ -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

@ -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

@ -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

@ -23,25 +23,25 @@ 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
}) })
.FirstOrDefault(c => c.NetworkId == networkId) .FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
); );
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
@ -235,10 +235,14 @@ 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)
{ {
if (altKey is not Reference.Game game)
{
throw new ArgumentException($"Alternate key must be of type {nameof(Reference.Game)}");
}
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
return await _getUniqueQuery(context, entityAttribute); return await GetUniqueQuery(context, entityAttribute, game);
} }
public async Task<EFClient> Update(EFClient temporalClient) public async Task<EFClient> Update(EFClient temporalClient)
@ -285,7 +289,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();
@ -758,19 +762,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)
@ -860,15 +865,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.6.9.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.6.9.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

@ -1181,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
}; };
} }
@ -1264,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;
@ -274,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"]}",

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,7 +88,7 @@ namespace WebfrontCore.Controllers
var clientDto = new PlayerInfo var clientDto = new PlayerInfo
{ {
Name = client.Name, Name = client.Name,
Game = client.GameName ?? Reference.Game.UKN, Game = client.GameName,
Level = displayLevel, Level = displayLevel,
LevelInt = displayLevelInt, LevelInt = displayLevelInt,
ClientId = client.ClientId, ClientId = client.ClientId,
@ -183,7 +183,7 @@ namespace WebfrontCore.Controllers
ClientId = admin.ClientId, ClientId = admin.ClientId,
LastConnection = admin.LastConnection, LastConnection = admin.LastConnection,
IsMasked = admin.Masked, IsMasked = admin.Masked,
Game = admin.GameName ?? Reference.Game.UKN Game = admin.GameName
}); });
} }

View File

@ -34,7 +34,12 @@ namespace WebfrontCore.Controllers
{ {
ClientId = id, ClientId = id,
ServerEndpoint = serverId ServerEndpoint = serverId
})).Results.First(); }))?.Results?.First();
if (hitInfo is null)
{
return NotFound();
}
var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId); var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
long? matchedServerId = null; long? matchedServerId = null;

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

@ -54,7 +54,8 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
client.NetworkId, client.NetworkId,
client.AliasLinkId, client.AliasLinkId,
client.ClientId, client.ClientId,
client.CurrentAlias.IPAddress client.CurrentAlias.IPAddress,
client.GameName
}).ToListAsync(); }).ToListAsync();
var results = new List<BanInfo>(); var results = new List<BanInfo>();
@ -101,7 +102,6 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
.Select(alias => alias.LinkId) .Select(alias => alias.LinkId)
.ToListAsync()).Distinct(); .ToListAsync()).Distinct();
matchedPenalties = await context.Penalties.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Ban) matchedPenalties = await context.Penalties.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Ban)
.Where(penalty => penalty.Expires == null || penalty.Expires > lateDateTime) .Where(penalty => penalty.Expires == null || penalty.Expires > lateDateTime)
.Where(penalty => penalty.LinkId != null && linkIds.Contains(penalty.LinkId.Value)) .Where(penalty => penalty.LinkId != null && linkIds.Contains(penalty.LinkId.Value))
@ -158,6 +158,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
ClientId = matchingClient.ClientId, ClientId = matchingClient.ClientId,
NetworkId = matchingClient.NetworkId, NetworkId = matchingClient.NetworkId,
IPAddress = matchingClient.IPAddress, IPAddress = matchingClient.IPAddress,
Game = matchingClient.GameName,
AssociatedPenalties = relatedEntities, AssociatedPenalties = relatedEntities,
AttachedPenalty = allPenalties.FirstOrDefault(penalty => AttachedPenalty = allPenalties.FirstOrDefault(penalty =>

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Data.Models;
namespace WebfrontCore.QueryHelpers.Models; namespace WebfrontCore.QueryHelpers.Models;
@ -9,6 +10,7 @@ public class BanInfo
public int ClientId { get; set; } public int ClientId { get; set; }
public int? IPAddress { get; set; } public int? IPAddress { get; set; }
public long NetworkId { get; set; } public long NetworkId { get; set; }
public Reference.Game Game { get; set; }
public PenaltyInfo AttachedPenalty { get; set; } public PenaltyInfo AttachedPenalty { get; set; }
public IEnumerable<PenaltyInfo> AssociatedPenalties { get; set; } public IEnumerable<PenaltyInfo> AssociatedPenalties { get; set; }
} }

View File

@ -12,22 +12,23 @@
<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="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 "> <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> <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"> <has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@ban.NetworkId.ToString("X")</div> <div class="text-muted">@ban.NetworkId.ToString("X")</div>
</has-permission> </has-permission>
<has-permission entity="ClientIPAddress" required-permission="Read"> <has-permission entity="ClientIPAddress" required-permission="Read">
<div class="text-muted">@ban.IPAddress.ConvertIPtoString()</div> <div class="text-muted">@ban.IPAddress.ConvertIPtoString()</div>
</has-permission> </has-permission>
<br/>
@if (ban.AttachedPenalty is not null) @if (ban.AttachedPenalty is not null)
{ {
<br/>
<div class="text-muted font-weight-light">@ban.AttachedPenalty.Offense.CapClientName(30)</div> <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="text-danger font-weight-light">@ban.AttachedPenalty.DateTime.ToStandardFormat()</div>
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@ban.ClientId">Unban</div> <div class="btn profile-action w-100" data-action="unban" data-action-id="@ban.ClientId">Unban</div>
} }
else else
{ {
<br/>
<div class="align-self-end text-muted font-weight-light"> <div class="align-self-end text-muted font-weight-light">
<span class="oi oi-warning font-size-12"></span> <span class="oi oi-warning font-size-12"></span>
<span>Link-Only Ban</span> <span>Link-Only Ban</span>
@ -56,6 +57,7 @@
<div class="text-muted font-weight-light">@associatedEntity.Offense.CapClientName(30)</div> <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="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="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> </div>

View File

@ -15,9 +15,7 @@
foreach (var snapshot in Model.ClientHistory.ClientCounts) foreach (var snapshot in Model.ClientHistory.ClientCounts)
{ {
snapshot.MapAlias = GetMapName(snapshot.Map); snapshot.MapAlias = GetMapName(snapshot.Map);
}; }
string MakeAbbreviation(string gameName) => string.Join("", gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
} }
<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">
@ -44,7 +42,7 @@
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}"]">@MakeAbbreviation(ViewBag.Localization[$"GAME_{Model.Game}"])</span> <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>
<!-- second column --> <!-- second column -->