update schema to support unique guid + game combinations
This commit is contained in:
parent
deeb1dea87
commit
8ae6561f4e
@ -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()
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 =>
|
||||||
{
|
{
|
||||||
|
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
});
|
});
|
||||||
|
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
});
|
});
|
||||||
|
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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; }
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal file
11
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal 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; }
|
||||||
|
}
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal file
11
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal 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; }
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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())
|
||||||
{
|
{
|
||||||
|
@ -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>
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"]}",
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 =>
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user