Add Steam auth, platform validators & name gen
This commit is contained in:
@@ -9,5 +9,4 @@ public class LoginRequest
|
|||||||
public required string PlatformAuthentication { get; set; }
|
public required string PlatformAuthentication { get; set; }
|
||||||
public required string PlatformId { get; set; }
|
public required string PlatformId { get; set; }
|
||||||
public PlatformType PlatformType { get; set; }
|
public PlatformType PlatformType { get; set; }
|
||||||
public required string Username { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ namespace RecNet.Application.Common.Interfaces;
|
|||||||
|
|
||||||
public interface IConfigService
|
public interface IConfigService
|
||||||
{
|
{
|
||||||
Task<T?> GetAsync<T>(string key, T? defaultValue = default, CancellationToken ct = default);
|
Task<T?> GetAsync<T>(string key, CancellationToken ct = default);
|
||||||
|
Task<T> GetAsync<T>(string key, T defaultValue, CancellationToken ct = default);
|
||||||
Task SetAsync<T>(string key, T value, CancellationToken ct = default);
|
Task SetAsync<T>(string key, T value, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using RecNet.Application.Common.PlatformAuth;
|
||||||
|
using RecNet.Domain.Common;
|
||||||
|
|
||||||
|
namespace RecNet.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
public interface IPlatformAuthValidator
|
||||||
|
{
|
||||||
|
PlatformType PlatformType { get; }
|
||||||
|
|
||||||
|
Task<PlatformAuthResult> ValidateAsync(
|
||||||
|
string platformAuthentication,
|
||||||
|
string platformId,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
11
RecNet.Application/Common/Interfaces/ISteamAuthService.cs
Normal file
11
RecNet.Application/Common/Interfaces/ISteamAuthService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using RecNet.Application.Common.Steam;
|
||||||
|
|
||||||
|
namespace RecNet.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
public interface ISteamAuthService
|
||||||
|
{
|
||||||
|
Task<SteamAuthResult> AuthorizeAsync(
|
||||||
|
string ticket,
|
||||||
|
uint appId,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
}
|
||||||
21
RecNet.Application/Common/PlatformAuth/PlatformAuthResult.cs
Normal file
21
RecNet.Application/Common/PlatformAuth/PlatformAuthResult.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace RecNet.Application.Common.PlatformAuth;
|
||||||
|
|
||||||
|
public class PlatformAuthResult
|
||||||
|
{
|
||||||
|
public bool Succeeded { get; private set; }
|
||||||
|
public string? Message { get; private set; }
|
||||||
|
public string? Name { get; private set; }
|
||||||
|
|
||||||
|
private PlatformAuthResult(bool succeeded, string? message, string? name)
|
||||||
|
{
|
||||||
|
Succeeded = succeeded;
|
||||||
|
Message = message;
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlatformAuthResult Success(string? name)
|
||||||
|
=> new(true, null, name);
|
||||||
|
|
||||||
|
public static PlatformAuthResult Failure(string? message = null)
|
||||||
|
=> new(false, message, null);
|
||||||
|
}
|
||||||
21
RecNet.Application/Common/Steam/SteamAuthResult.cs
Normal file
21
RecNet.Application/Common/Steam/SteamAuthResult.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace RecNet.Application.Common.Steam;
|
||||||
|
|
||||||
|
public class SteamAuthResult
|
||||||
|
{
|
||||||
|
public bool Succeeded { get; private set; }
|
||||||
|
public string? SteamId { get; private set; }
|
||||||
|
public string? DisplayName { get; private set; }
|
||||||
|
|
||||||
|
private SteamAuthResult(bool succeeded, string? steamId, string? displayName)
|
||||||
|
{
|
||||||
|
Succeeded = succeeded;
|
||||||
|
SteamId = steamId;
|
||||||
|
DisplayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SteamAuthResult Success(string steamId, string? displayName)
|
||||||
|
=> new(true, steamId, displayName);
|
||||||
|
|
||||||
|
public static SteamAuthResult Failure()
|
||||||
|
=> new (false, null, null);
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ public static class DependencyInjection
|
|||||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
public static IServiceCollection AddApplication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddAutoMapper(_ => { }, typeof(DependencyInjection).Assembly);
|
services.AddAutoMapper(_ => { }, typeof(DependencyInjection).Assembly);
|
||||||
|
|
||||||
|
services.AddScoped<NameGenerator>();
|
||||||
|
|
||||||
services.AddScoped<IProfileService, ProfileService>();
|
services.AddScoped<IProfileService, ProfileService>();
|
||||||
services.AddScoped<INeutrinoAuthorizationService, NeutrinoAuthorizationService>();
|
services.AddScoped<INeutrinoAuthorizationService, NeutrinoAuthorizationService>();
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class NeutrinoAuthorizationService(
|
|||||||
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
||||||
|
|
||||||
var profile = await profileRepository.GetByIdAsync(command.ProfileId, ct);
|
var profile = await profileRepository.GetByIdAsync(command.ProfileId, ct);
|
||||||
if (profile is null)
|
if (profile is null || profile.IsBanned)
|
||||||
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
||||||
|
|
||||||
return NeutrinoAuthorizationResult.Success(profile.ProfileId, profile.Name);
|
return NeutrinoAuthorizationResult.Success(profile.ProfileId, profile.Name);
|
||||||
|
|||||||
17
RecNet.Application/Profiles/NameGenerator.cs
Normal file
17
RecNet.Application/Profiles/NameGenerator.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using RecNet.Application.Common.Interfaces;
|
||||||
|
using RecNet.Domain.Entities.Profiles.Names;
|
||||||
|
|
||||||
|
namespace RecNet.Application.Profiles;
|
||||||
|
|
||||||
|
public class NameGenerator(IConfigService configService)
|
||||||
|
{
|
||||||
|
public async Task<string> GenerateAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var config = await configService.GetAsync("Profiles:NameGen", DefaultNameGenConfig.Value, ct);
|
||||||
|
|
||||||
|
var adjective = config.Adjectives[Random.Shared.Next(config.Adjectives.Count)];
|
||||||
|
var noun = config.Nouns[Random.Shared.Next(config.Nouns.Count)];
|
||||||
|
|
||||||
|
return $"{adjective}{noun}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace RecNet.Application.Profiles;
|
namespace RecNet.Application.Profiles;
|
||||||
|
|
||||||
public class ProfileDTO
|
public class ProfileDTO
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("ProfileId")]
|
||||||
public Guid ProfileId { get; init; }
|
public Guid ProfileId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("Name")]
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ using ProfileEntity = RecNet.Domain.Entities.Profiles.Profile;
|
|||||||
namespace RecNet.Application.Profiles;
|
namespace RecNet.Application.Profiles;
|
||||||
|
|
||||||
public class ProfileService(
|
public class ProfileService(
|
||||||
|
NameGenerator nameGenerator,
|
||||||
IProfileRepository profileRepository,
|
IProfileRepository profileRepository,
|
||||||
ITokenService tokenService,
|
ITokenService tokenService,
|
||||||
|
IConfigService configService,
|
||||||
|
IEnumerable<IPlatformAuthValidator> validators,
|
||||||
IMapper mapper) : IProfileService
|
IMapper mapper) : IProfileService
|
||||||
{
|
{
|
||||||
public async Task<ProfileDTO?> GetProfileAsync(Guid profileId, CancellationToken ct = default)
|
public async Task<ProfileDTO?> GetProfileAsync(Guid profileId, CancellationToken ct = default)
|
||||||
@@ -32,6 +35,19 @@ public class ProfileService(
|
|||||||
LoginProfileCommand command,
|
LoginProfileCommand command,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
var validator = validators.FirstOrDefault(x => x.PlatformType == command.PlatformType);
|
||||||
|
if (validator is null)
|
||||||
|
return LoginProfileResult.Fail("Invalid platform type");
|
||||||
|
|
||||||
|
var ignoreAuthValidation = await configService.GetAsync("Profiles:IgnoreAuthValidation", true, ct);
|
||||||
|
|
||||||
|
var auth = await validator.ValidateAsync(
|
||||||
|
command.PlatformAuthentication,
|
||||||
|
command.PlatformId,
|
||||||
|
ct);
|
||||||
|
if (!auth.Succeeded && !ignoreAuthValidation)
|
||||||
|
return LoginProfileResult.Fail($"Platform Auth Failed: {auth.Message}");
|
||||||
|
|
||||||
var profile = await profileRepository.GetByPlatform(
|
var profile = await profileRepository.GetByPlatform(
|
||||||
command.PlatformType,
|
command.PlatformType,
|
||||||
command.PlatformId,
|
command.PlatformId,
|
||||||
@@ -39,21 +55,26 @@ public class ProfileService(
|
|||||||
|
|
||||||
if (profile is null)
|
if (profile is null)
|
||||||
{
|
{
|
||||||
|
var name = await nameGenerator.GenerateAsync(ct);
|
||||||
|
|
||||||
profile = ProfileEntity.Create(
|
profile = ProfileEntity.Create(
|
||||||
GenerateRandomName(),
|
name,
|
||||||
command.PlatformType,
|
command.PlatformType,
|
||||||
command.PlatformId);
|
command.PlatformId);
|
||||||
|
|
||||||
await profileRepository.AddAsync(profile, ct);
|
await profileRepository.AddAsync(profile, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile.IsBanned)
|
||||||
|
return LoginProfileResult.Fail("Profile is banned");
|
||||||
|
|
||||||
|
if (auth.Name is not null && profile.Name != auth.Name)
|
||||||
|
profile.SetName(auth.Name);
|
||||||
|
|
||||||
profile.AddDeviceId(command.DeviceId);
|
profile.AddDeviceId(command.DeviceId);
|
||||||
|
|
||||||
await profileRepository.SaveChangesAsync(ct);
|
await profileRepository.SaveChangesAsync(ct);
|
||||||
|
|
||||||
if (profile.IsBanned)
|
|
||||||
return LoginProfileResult.Fail("Profile is banned");
|
|
||||||
|
|
||||||
var token = tokenService.GenerateProfileToken(profile);
|
var token = tokenService.GenerateProfileToken(profile);
|
||||||
|
|
||||||
return LoginProfileResult.Success(
|
return LoginProfileResult.Success(
|
||||||
@@ -62,8 +83,4 @@ public class ProfileService(
|
|||||||
expiresIn: token.ExpiresIn
|
expiresIn: token.ExpiresIn
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary until platform/user-name verification becomes a real use case.
|
|
||||||
private static string GenerateRandomName()
|
|
||||||
=> string.Concat("rr", Guid.NewGuid().ToString("N").AsSpan(0, 18));
|
|
||||||
}
|
}
|
||||||
@@ -14,4 +14,8 @@
|
|||||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Authentication\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
288
RecNet.Domain/Entities/Profiles/Names/DefaultNameGenConfig.cs
Normal file
288
RecNet.Domain/Entities/Profiles/Names/DefaultNameGenConfig.cs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
namespace RecNet.Domain.Entities.Profiles.Names;
|
||||||
|
|
||||||
|
public static class DefaultNameGenConfig
|
||||||
|
{
|
||||||
|
public static readonly NameGenConfig Value = new()
|
||||||
|
{
|
||||||
|
Adjectives =
|
||||||
|
[
|
||||||
|
"Adamant",
|
||||||
|
"Adorable",
|
||||||
|
"Adventurous",
|
||||||
|
"Agreeable",
|
||||||
|
"Aimless",
|
||||||
|
"Alert",
|
||||||
|
"Amused",
|
||||||
|
"Aromatic",
|
||||||
|
"Bashful",
|
||||||
|
"Beautiful",
|
||||||
|
"Bored",
|
||||||
|
"Brave",
|
||||||
|
"Bulbous",
|
||||||
|
"Busy",
|
||||||
|
"Calm",
|
||||||
|
"Carefree",
|
||||||
|
"Careless",
|
||||||
|
"Caring",
|
||||||
|
"Charming",
|
||||||
|
"Cheerful",
|
||||||
|
"Clever",
|
||||||
|
"Clumsy",
|
||||||
|
"Courageous",
|
||||||
|
"Cowardly",
|
||||||
|
"Cozy",
|
||||||
|
"Crabby",
|
||||||
|
"Cranky",
|
||||||
|
"Crawling",
|
||||||
|
"Creaky",
|
||||||
|
"Creative",
|
||||||
|
"Crispy",
|
||||||
|
"Decisive",
|
||||||
|
"Deep",
|
||||||
|
"Delightful",
|
||||||
|
"Determined",
|
||||||
|
"Diligent",
|
||||||
|
"Dull",
|
||||||
|
"Eager",
|
||||||
|
"Elated",
|
||||||
|
"Emotional",
|
||||||
|
"Enchanting",
|
||||||
|
"Encouraging",
|
||||||
|
"Endless",
|
||||||
|
"Energetic",
|
||||||
|
"Enthusiastic",
|
||||||
|
"Excited",
|
||||||
|
"Exuberant",
|
||||||
|
"Fair",
|
||||||
|
"Faithful",
|
||||||
|
"Fantastic",
|
||||||
|
"Fastidious",
|
||||||
|
"Fine",
|
||||||
|
"Fluttering",
|
||||||
|
"Fragrant",
|
||||||
|
"Friendly",
|
||||||
|
"Fulsome",
|
||||||
|
"Funny",
|
||||||
|
"Fussy",
|
||||||
|
"Fuzzy",
|
||||||
|
"Generous",
|
||||||
|
"Gentle",
|
||||||
|
"Glassy",
|
||||||
|
"Gloomy",
|
||||||
|
"Glorious",
|
||||||
|
"Radiating",
|
||||||
|
"Glowing",
|
||||||
|
"Good",
|
||||||
|
"Grand",
|
||||||
|
"Great",
|
||||||
|
"Greedy",
|
||||||
|
"Grimy",
|
||||||
|
"Happy",
|
||||||
|
"Hardworking",
|
||||||
|
"Hasty",
|
||||||
|
"Healthy",
|
||||||
|
"Heavy",
|
||||||
|
"Helpful",
|
||||||
|
"Hilarious",
|
||||||
|
"Hopeful",
|
||||||
|
"Icy",
|
||||||
|
"Important",
|
||||||
|
"Inquisitive",
|
||||||
|
"Jolly",
|
||||||
|
"Joyful",
|
||||||
|
"Joyous",
|
||||||
|
"Kind",
|
||||||
|
"Lazy",
|
||||||
|
"Lively",
|
||||||
|
"Loud",
|
||||||
|
"Lovely",
|
||||||
|
"Loyal",
|
||||||
|
"Lucky",
|
||||||
|
"Luminous",
|
||||||
|
"Lumpy",
|
||||||
|
"Majestic",
|
||||||
|
"Meek",
|
||||||
|
"Melodic",
|
||||||
|
"Mighty",
|
||||||
|
"Moody",
|
||||||
|
"Nice",
|
||||||
|
"Nimble",
|
||||||
|
"Odd",
|
||||||
|
"Optimistic",
|
||||||
|
"Perfect",
|
||||||
|
"Pervasive",
|
||||||
|
"Pleasant",
|
||||||
|
"Plucky",
|
||||||
|
"Plush",
|
||||||
|
"Polite",
|
||||||
|
"Practical",
|
||||||
|
"Proud",
|
||||||
|
"Quick",
|
||||||
|
"Quiet",
|
||||||
|
"Rapid",
|
||||||
|
"Redolent",
|
||||||
|
"Reliable",
|
||||||
|
"Relieved",
|
||||||
|
"Royal",
|
||||||
|
"Rusty",
|
||||||
|
"Scared",
|
||||||
|
"Selfish",
|
||||||
|
"Sensible",
|
||||||
|
"Sensitive",
|
||||||
|
"Shining",
|
||||||
|
"Shrill",
|
||||||
|
"Silly",
|
||||||
|
"Sincere",
|
||||||
|
"Sizzling",
|
||||||
|
"Sleepy",
|
||||||
|
"Smiling",
|
||||||
|
"Smooth",
|
||||||
|
"Snug",
|
||||||
|
"Soaring",
|
||||||
|
"Sparkling",
|
||||||
|
"Speedy",
|
||||||
|
"Spiky",
|
||||||
|
"Splendid",
|
||||||
|
"Spoiled",
|
||||||
|
"Steaming",
|
||||||
|
"Still",
|
||||||
|
"Strict",
|
||||||
|
"Stuffed",
|
||||||
|
"Sturdy",
|
||||||
|
"Successful",
|
||||||
|
"Surprised",
|
||||||
|
"Swift",
|
||||||
|
"Taciturn",
|
||||||
|
"Tense",
|
||||||
|
"Thankful",
|
||||||
|
"Thoughtful",
|
||||||
|
"Thrifty",
|
||||||
|
"Tough",
|
||||||
|
"Tricky",
|
||||||
|
"Truthful",
|
||||||
|
"Ubiquitous",
|
||||||
|
"Unusual",
|
||||||
|
"Versatile",
|
||||||
|
"Victorious",
|
||||||
|
"Wild",
|
||||||
|
"Wise",
|
||||||
|
"Witty",
|
||||||
|
"Wonderful",
|
||||||
|
"Worried",
|
||||||
|
"Wrinkly",
|
||||||
|
"Zany",
|
||||||
|
"Zealous"
|
||||||
|
],
|
||||||
|
Nouns =
|
||||||
|
[
|
||||||
|
"Aardvark",
|
||||||
|
"Alpaca",
|
||||||
|
"Ant",
|
||||||
|
"Armadillo",
|
||||||
|
"Badger",
|
||||||
|
"Bat",
|
||||||
|
"Bear",
|
||||||
|
"Bee",
|
||||||
|
"Buffalo",
|
||||||
|
"Butterfly",
|
||||||
|
"Capybara",
|
||||||
|
"Cat",
|
||||||
|
"Caterpillar",
|
||||||
|
"Chameleon",
|
||||||
|
"Cheetah",
|
||||||
|
"Chicken",
|
||||||
|
"Chimpanzee",
|
||||||
|
"Cobra",
|
||||||
|
"Coyote",
|
||||||
|
"Crane",
|
||||||
|
"Cricket",
|
||||||
|
"Crow",
|
||||||
|
"Deer",
|
||||||
|
"Dog",
|
||||||
|
"Dolphin",
|
||||||
|
"Donkey",
|
||||||
|
"Dove",
|
||||||
|
"Duck",
|
||||||
|
"Eagle",
|
||||||
|
"Echidna",
|
||||||
|
"Elephant",
|
||||||
|
"Elk",
|
||||||
|
"Emu",
|
||||||
|
"Ferret",
|
||||||
|
"Flamingo",
|
||||||
|
"Fish",
|
||||||
|
"Fox",
|
||||||
|
"Frog",
|
||||||
|
"Gazelle",
|
||||||
|
"Giraffe",
|
||||||
|
"Goat",
|
||||||
|
"Goose",
|
||||||
|
"Gorilla",
|
||||||
|
"Hamster",
|
||||||
|
"Hedgehog",
|
||||||
|
"Hippo",
|
||||||
|
"Horse",
|
||||||
|
"Hyena",
|
||||||
|
"Iguana",
|
||||||
|
"Jaguar",
|
||||||
|
"Jellyfish",
|
||||||
|
"Kangaroo",
|
||||||
|
"Kitten",
|
||||||
|
"Koala",
|
||||||
|
"Lemming",
|
||||||
|
"Leopard",
|
||||||
|
"Lion",
|
||||||
|
"Lizard",
|
||||||
|
"Llama",
|
||||||
|
"Marmoset",
|
||||||
|
"Monkey",
|
||||||
|
"Moose",
|
||||||
|
"Mouse",
|
||||||
|
"Mule",
|
||||||
|
"Newt",
|
||||||
|
"Octopus",
|
||||||
|
"Opposum",
|
||||||
|
"Ostrich",
|
||||||
|
"Otter",
|
||||||
|
"Owl",
|
||||||
|
"Oyster",
|
||||||
|
"Panda",
|
||||||
|
"Panther",
|
||||||
|
"Parrot",
|
||||||
|
"Penguin",
|
||||||
|
"Pig",
|
||||||
|
"Pigeon",
|
||||||
|
"Piranha",
|
||||||
|
"Platypus",
|
||||||
|
"Pony",
|
||||||
|
"Possum",
|
||||||
|
"Puppy",
|
||||||
|
"Quail",
|
||||||
|
"Rabbit",
|
||||||
|
"Raven",
|
||||||
|
"Salmon",
|
||||||
|
"Scorpion",
|
||||||
|
"Seal",
|
||||||
|
"Shark",
|
||||||
|
"Sheep",
|
||||||
|
"Sloth",
|
||||||
|
"Snail",
|
||||||
|
"Squid",
|
||||||
|
"Squirrel",
|
||||||
|
"Stork",
|
||||||
|
"Tapir",
|
||||||
|
"Tiger",
|
||||||
|
"Tortoise",
|
||||||
|
"Tuna",
|
||||||
|
"Turtle",
|
||||||
|
"Urchin",
|
||||||
|
"Viper",
|
||||||
|
"Vulture",
|
||||||
|
"Walrus",
|
||||||
|
"Whale",
|
||||||
|
"Wombat",
|
||||||
|
"Yak",
|
||||||
|
"Zebra"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
7
RecNet.Domain/Entities/Profiles/Names/NameGenConfig.cs
Normal file
7
RecNet.Domain/Entities/Profiles/Names/NameGenConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace RecNet.Domain.Entities.Profiles.Names;
|
||||||
|
|
||||||
|
public class NameGenConfig
|
||||||
|
{
|
||||||
|
public required List<string> Adjectives { get; set; }
|
||||||
|
public required List<string> Nouns { get; set; }
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using RecNet.Domain.Repositories;
|
|||||||
using RecNet.Infrastructure.Persistence;
|
using RecNet.Infrastructure.Persistence;
|
||||||
using RecNet.Infrastructure.Persistence.Repositories;
|
using RecNet.Infrastructure.Persistence.Repositories;
|
||||||
using RecNet.Infrastructure.Services.Configuration;
|
using RecNet.Infrastructure.Services.Configuration;
|
||||||
|
using RecNet.Infrastructure.Services.Steam;
|
||||||
using RecNet.Infrastructure.Services.Tokens;
|
using RecNet.Infrastructure.Services.Tokens;
|
||||||
|
|
||||||
namespace RecNet.Infrastructure;
|
namespace RecNet.Infrastructure;
|
||||||
@@ -22,6 +23,17 @@ public static class DependencyInjection
|
|||||||
.Validate(options => !string.IsNullOrWhiteSpace(options.Secret), "JWT secret is required.")
|
.Validate(options => !string.IsNullOrWhiteSpace(options.Secret), "JWT secret is required.")
|
||||||
.ValidateOnStart();
|
.ValidateOnStart();
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddOptions<SteamOptions>()
|
||||||
|
.Bind(builder.Configuration.GetSection("Steam"));
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<ISteamAuthService, SteamAuthService>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri("https://api.steampowered.com/");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IPlatformAuthValidator, SteamAuthValidator>();
|
||||||
|
|
||||||
builder.Services.AddScoped<IProfileRepository, ProfileRepository>();
|
builder.Services.AddScoped<IProfileRepository, ProfileRepository>();
|
||||||
builder.Services.AddScoped<IServerConfigRepository, ServerConfigRepository>();
|
builder.Services.AddScoped<IServerConfigRepository, ServerConfigRepository>();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.4.5" />
|
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.4.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
|
||||||
|
<PackageReference Include="SteamApi.Models" Version="1.1.1" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,25 @@ public class ConfigService(IServerConfigRepository repository) : IConfigService
|
|||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
public async Task<T?> GetAsync<T>(
|
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var config = await repository.GetAsync(key, ct);
|
||||||
|
if (config == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> GetAsync<T>(
|
||||||
string key,
|
string key,
|
||||||
T? defaultValue = default,
|
T defaultValue,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var config = await repository.GetAsync(key, ct);
|
var config = await repository.GetAsync(key, ct);
|
||||||
if (config == null)
|
if (config == null)
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions);
|
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions) ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetAsync<T>(
|
public async Task SetAsync<T>(
|
||||||
|
|||||||
89
RecNet.Infrastructure/Services/Steam/SteamAuthService.cs
Normal file
89
RecNet.Infrastructure/Services/Steam/SteamAuthService.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using RecNet.Application.Common.Interfaces;
|
||||||
|
using RecNet.Application.Common.Steam;
|
||||||
|
using SteamApi.Models.Steam.Player;
|
||||||
|
using SteamApi.Models.Steam.Responses;
|
||||||
|
|
||||||
|
namespace RecNet.Infrastructure.Services.Steam;
|
||||||
|
|
||||||
|
public class SteamAuthService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
IOptions<SteamOptions> options) : ISteamAuthService
|
||||||
|
{
|
||||||
|
private readonly SteamOptions _options = options.Value;
|
||||||
|
|
||||||
|
public async Task<SteamAuthResult> AuthorizeAsync(
|
||||||
|
string ticket,
|
||||||
|
uint appId,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ticket))
|
||||||
|
return SteamAuthResult.Failure();
|
||||||
|
|
||||||
|
if (appId != _options.AppId)
|
||||||
|
return SteamAuthResult.Failure();
|
||||||
|
|
||||||
|
var steamId = await AuthenticateTicketAsync(ticket, ct);
|
||||||
|
if (steamId is null)
|
||||||
|
return SteamAuthResult.Failure();
|
||||||
|
|
||||||
|
var profile = await GetProfileAsync(steamId, ct);
|
||||||
|
|
||||||
|
return SteamAuthResult.Success(
|
||||||
|
steamId,
|
||||||
|
profile?.PersonaName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> AuthenticateTicketAsync(
|
||||||
|
string ticket,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var url =
|
||||||
|
"ISteamUserAuth/AuthenticateUserTicket/v0001/" +
|
||||||
|
$"?key={Uri.EscapeDataString(_options.ApiKey)}" +
|
||||||
|
$"&appid={_options.AppId}" +
|
||||||
|
$"&ticket={Uri.EscapeDataString(ticket)}";
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync(url, ct);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<SteamResponse<SteamAuthResponse>>(
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
return body?.Response?.Params?.SteamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PlayerSummary?> GetProfileAsync(
|
||||||
|
string steamId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var url =
|
||||||
|
"ISteamUser/GetPlayerSummaries/v0002/" +
|
||||||
|
$"?key={Uri.EscapeDataString(_options.ApiKey)}" +
|
||||||
|
$"&steamids={Uri.EscapeDataString(steamId)}";
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync(url, ct);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<SteamResponse<PlayerSummariesResponse>>(
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
return body?.Response?.Players.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SteamAuthResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("params")]
|
||||||
|
public SteamAuthParamsResponse? Params { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SteamAuthParamsResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("steamid")]
|
||||||
|
public required string SteamId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
48
RecNet.Infrastructure/Services/Steam/SteamAuthValidator.cs
Normal file
48
RecNet.Infrastructure/Services/Steam/SteamAuthValidator.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using RecNet.Application.Common.Interfaces;
|
||||||
|
using RecNet.Application.Common.PlatformAuth;
|
||||||
|
using RecNet.Domain.Common;
|
||||||
|
|
||||||
|
namespace RecNet.Infrastructure.Services.Steam;
|
||||||
|
|
||||||
|
public class SteamAuthValidator(ISteamAuthService steamAuthService) : IPlatformAuthValidator
|
||||||
|
{
|
||||||
|
public PlatformType PlatformType
|
||||||
|
=> PlatformType.Steamworks;
|
||||||
|
|
||||||
|
public async Task<PlatformAuthResult> ValidateAsync(
|
||||||
|
string platformAuthentication,
|
||||||
|
string platformId,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var steamParams = JsonSerializer.Deserialize<SteamPlatformAuth>(platformAuthentication);
|
||||||
|
if (steamParams == null)
|
||||||
|
return PlatformAuthResult.Failure("Invalid authentication");
|
||||||
|
|
||||||
|
var result = await steamAuthService.AuthorizeAsync(steamParams.Ticket, steamParams.AppId.AppId, ct);
|
||||||
|
|
||||||
|
if (!result.Succeeded || result.SteamId is null)
|
||||||
|
return PlatformAuthResult.Failure("Invalid authentication");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(platformId) &&
|
||||||
|
platformId != result.SteamId)
|
||||||
|
return PlatformAuthResult.Failure("Nice try");
|
||||||
|
|
||||||
|
return PlatformAuthResult.Success(result.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SteamPlatformAuth
|
||||||
|
{
|
||||||
|
public required string Ticket { get; set; }
|
||||||
|
|
||||||
|
public required SteamAppIdAuth AppId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splooty, why did you set it up like this ;-;
|
||||||
|
private sealed class SteamAppIdAuth
|
||||||
|
{
|
||||||
|
[JsonPropertyName("m_AppId")]
|
||||||
|
public required uint AppId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
8
RecNet.Infrastructure/Services/Steam/SteamOptions.cs
Normal file
8
RecNet.Infrastructure/Services/Steam/SteamOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace RecNet.Infrastructure.Services.Steam;
|
||||||
|
|
||||||
|
public class SteamOptions
|
||||||
|
{
|
||||||
|
public string ApiKey { get; set; } = string.Empty;
|
||||||
|
public uint AppId { get; set; } = 480; // Spacewar
|
||||||
|
public string Identity { get; set; } = "recnet";
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ public class TokenService(IOptions<JwtOptions> options) : ITokenService
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (profile.IsModerator)
|
if (profile.IsModerator)
|
||||||
claims.Add(new Claim(ClaimTypes.Role, "moderator"));
|
claims.Add(new Claim("role", "moderator"));
|
||||||
|
|
||||||
var token = new JwtSecurityToken(
|
var token = new JwtSecurityToken(
|
||||||
issuer: jwtOptions.Issuer,
|
issuer: jwtOptions.Issuer,
|
||||||
|
|||||||
Reference in New Issue
Block a user