Added mocked SteamAuthMiddleware to allow impersonating a specific user when using an API client

Added more performant database logic for UpdateUserInfo
Added support for optional appsettings.user.json file for user-specific configuration
This commit is contained in:
Lennard Fonteijn 2023-10-27 03:01:10 +02:00
parent 107edbea8a
commit a3165539a5
13 changed files with 87 additions and 19 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ obj
# Server
/Src/Cobra.Server/Data
/Src/Cobra.Server/appsettings.user.json
# Other
/Res

View File

@ -10,6 +10,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=baller/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=categoryid/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=checkpointid/@EntryIndexedValue">True</s:Boolean>

View File

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.BulkExtensions" Version="7.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
</ItemGroup>

View File

@ -6,6 +6,6 @@ namespace Cobra.Server.Hitman.Interfaces
{
Task<GetUserOverviewData> GetUserOverviewData(ulong userId);
Task<int?> GetUserWallet(ulong userId);
Task UpdateUserInfo(ulong userId, string displayName, int country, List<ulong> friends);
Task UpdateUserInfo(ulong userId, string displayName, int country, HashSet<ulong> friends);
}
}

View File

@ -134,7 +134,7 @@ namespace Cobra.Server.Hitman.Services
var friends = request.Friends
.Select(ulong.Parse)
.ToList();
.ToHashSet();
//TODO: Validate if country is a valid id, this is a non-standard and will have to be extracted from the game.

View File

@ -2,6 +2,7 @@
using Cobra.Server.Database.Models;
using Cobra.Server.Hitman.Interfaces;
using Cobra.Server.Hitman.Models;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Hitman.Services
@ -45,12 +46,13 @@ namespace Cobra.Server.Hitman.Services
return user?.Wallet;
}
public async Task UpdateUserInfo(ulong userId, string displayName, int country, List<ulong> friends)
public async Task UpdateUserInfo(ulong userId, string displayName, int country, HashSet<ulong> friends)
{
await using var databaseContext = await _databaseContextFactory.CreateDbContextAsync();
var user = await databaseContext.Users
.Include(x => x.Friends)
.AsSplitQuery()
.FirstOrDefaultAsync(x => x.Id == userId);
if (user == null)
@ -66,14 +68,27 @@ namespace Cobra.Server.Hitman.Services
user.DisplayName = displayName;
user.Country = country;
//NOTE: This will effectively drop old friends and add new ones
user.Friends = friends.Select(x => new UserFriend
//Remove old friends
foreach (var userFriend in user.Friends.Where(friend => !friends.Contains(friend.SteamId)))
{
User = user,
SteamId = x
}).ToList();
databaseContext.Entry(userFriend).State = EntityState.Deleted;
}
await databaseContext.SaveChangesAsync();
//Add new friends
var existingFriends = user.Friends
.Select(x => x.SteamId)
.ToHashSet();
foreach (var friend in friends.Where(friend => !existingFriends.Contains(friend)))
{
user.Friends.Add(new UserFriend
{
User = user,
SteamId = friend
});
}
await databaseContext.BulkSaveChangesAsync();
}
}
}

View File

@ -302,13 +302,13 @@ namespace Cobra.Server.Hitman.Services
RichestAverage = 1337,
RichestRank = 1337,
TrophiesEarned = 1337,
WalletAmount = _options.WalletAmount
WalletAmount = _options.MockedWalletAmount
});
}
public Task<int> GetUserWallet(GetUserWalletRequest request)
{
return Task.FromResult(_options.WalletAmount);
return Task.FromResult(_options.MockedWalletAmount);
}
public void InviteToCompetition(InviteToCompetitionRequest request)

View File

@ -12,6 +12,7 @@
public enum ESteamService
{
None = 0,
Mocked,
GameServer,
WebApi
}
@ -20,12 +21,16 @@
public bool EnableRequestLogging { get; set; } = false;
public bool EnableRequestBodyLogging { get; set; } = false;
public bool EnableResponseBodyLogging { get; set; } = false;
public string MockedContractSteamId { get; set; } = "76561198161220058";
public int WalletAmount { get; set; } = 1337;
public EGameService GameService { get; set; } = EGameService.Mocked;
public int JwtTokenExpirationInSeconds { get; set; } = 60 * 60 * 8; //NOTE: 8 hours
public string JwtSignKey { get; set; } = Guid.NewGuid().ToString();
public EGameService GameService { get; set; } = EGameService.Mocked;
public ESteamService SteamService { get; set; } = ESteamService.None;
public string SteamWebApiKey { get; set; } = string.Empty;
public int MockedWalletAmount { get; set; } = 1337;
public string MockedContractSteamId { get; set; } = "76561198161220058";
public ulong MockedSteamServiceSteamId { get; set; } = 76561197989140534;
}
}

View File

@ -0,0 +1,7 @@
namespace Cobra.Server.Interfaces
{
public interface ISteamAuthMiddleware : IMiddleware
{
//Do nothing
}
}

View File

@ -0,0 +1,24 @@
using System.Security.Claims;
using Cobra.Server.Interfaces;
using Cobra.Server.Models;
using Cobra.Server.Shared.Models;
namespace Cobra.Server.Mvc
{
public class MockedSteamAuthMiddleware : ISteamAuthMiddleware
{
private readonly Options _options;
public MockedSteamAuthMiddleware(Options options)
{
_options = options;
}
public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
context.User = new ClaimsPrincipal(new CustomIdentity(_options.MockedSteamServiceSteamId));
return next(context);
}
}
}

View File

@ -8,7 +8,7 @@ using Cobra.Server.Shared.Models;
namespace Cobra.Server.Mvc
{
public class SteamAuthMiddleware : IMiddleware
public class SteamAuthMiddleware : ISteamAuthMiddleware
{
private sealed class JwtToken
{

View File

@ -13,6 +13,10 @@ namespace Cobra.Server
{
return WebHost
.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(builder =>
{
builder.AddJsonFile("appsettings.user.json", true);
})
.UseKestrel(options =>
{
//NOTE: Since the game doesn't actually POST data...

View File

@ -73,9 +73,19 @@ namespace Cobra.Server
}
//Middleware
services.AddTransient<FixAddMetricsContentTypeMiddleware>();
services.AddTransient<RequestResponseLoggerMiddleware>();
services.AddTransient<SteamAuthMiddleware>();
services.AddSingleton<FixAddMetricsContentTypeMiddleware>();
services.AddSingleton<RequestResponseLoggerMiddleware>();
switch (options.SteamService)
{
case Options.ESteamService.Mocked:
services.AddSingleton<ISteamAuthMiddleware, MockedSteamAuthMiddleware>();
break;
case Options.ESteamService.GameServer:
case Options.ESteamService.WebApi:
services.AddSingleton<ISteamAuthMiddleware, SteamAuthMiddleware>();
break;
}
//Compositions
DatabaseCompositions.ConfigureServices(services, _configuration, options);
@ -105,7 +115,7 @@ namespace Cobra.Server
if (options.SteamService != Options.ESteamService.None)
{
app.UseMiddleware<SteamAuthMiddleware>();
app.UseMiddleware<ISteamAuthMiddleware>();
}
app.UseMvc();