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 PlatformId { get; set; }
|
||||
public PlatformType PlatformType { get; set; }
|
||||
public required string Username { get; set; }
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class NeutrinoController(INeutrinoAuthorizationService authorizationServi
|
||||
request.ProfileId,
|
||||
request.AccessToken),
|
||||
ct);
|
||||
|
||||
|
||||
if (!result.Succeeded)
|
||||
return Ok(
|
||||
NeutrinoAuthenticateResponse.Failure(
|
||||
|
||||
@@ -2,6 +2,7 @@ namespace RecNet.Application.Common.Interfaces;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
services.AddAutoMapper(_ => { }, typeof(DependencyInjection).Assembly);
|
||||
|
||||
services.AddScoped<NameGenerator>();
|
||||
|
||||
services.AddScoped<IProfileService, ProfileService>();
|
||||
services.AddScoped<INeutrinoAuthorizationService, NeutrinoAuthorizationService>();
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ public class NeutrinoAuthorizationService(
|
||||
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
||||
|
||||
var profile = await profileRepository.GetByIdAsync(command.ProfileId, ct);
|
||||
if (profile is null)
|
||||
if (profile is null || profile.IsBanned)
|
||||
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
||||
|
||||
|
||||
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;
|
||||
|
||||
public class ProfileDTO
|
||||
{
|
||||
[JsonPropertyName("ProfileId")]
|
||||
public Guid ProfileId { get; init; }
|
||||
|
||||
[JsonPropertyName("Name")]
|
||||
public required string Name { get; init; }
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ using ProfileEntity = RecNet.Domain.Entities.Profiles.Profile;
|
||||
namespace RecNet.Application.Profiles;
|
||||
|
||||
public class ProfileService(
|
||||
NameGenerator nameGenerator,
|
||||
IProfileRepository profileRepository,
|
||||
ITokenService tokenService,
|
||||
IConfigService configService,
|
||||
IEnumerable<IPlatformAuthValidator> validators,
|
||||
IMapper mapper) : IProfileService
|
||||
{
|
||||
public async Task<ProfileDTO?> GetProfileAsync(Guid profileId, CancellationToken ct = default)
|
||||
@@ -32,6 +35,19 @@ public class ProfileService(
|
||||
LoginProfileCommand command,
|
||||
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(
|
||||
command.PlatformType,
|
||||
command.PlatformId,
|
||||
@@ -39,21 +55,26 @@ public class ProfileService(
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
var name = await nameGenerator.GenerateAsync(ct);
|
||||
|
||||
profile = ProfileEntity.Create(
|
||||
GenerateRandomName(),
|
||||
name,
|
||||
command.PlatformType,
|
||||
command.PlatformId);
|
||||
|
||||
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);
|
||||
|
||||
await profileRepository.SaveChangesAsync(ct);
|
||||
|
||||
if (profile.IsBanned)
|
||||
return LoginProfileResult.Fail("Profile is banned");
|
||||
|
||||
var token = tokenService.GenerateProfileToken(profile);
|
||||
|
||||
return LoginProfileResult.Success(
|
||||
@@ -62,8 +83,4 @@ public class ProfileService(
|
||||
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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Authentication\" />
|
||||
</ItemGroup>
|
||||
|
||||
</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.Repositories;
|
||||
using RecNet.Infrastructure.Services.Configuration;
|
||||
using RecNet.Infrastructure.Services.Steam;
|
||||
using RecNet.Infrastructure.Services.Tokens;
|
||||
|
||||
namespace RecNet.Infrastructure;
|
||||
@@ -21,6 +22,17 @@ public static class DependencyInjection
|
||||
.Bind(builder.Configuration.GetSection("Jwt"))
|
||||
.Validate(options => !string.IsNullOrWhiteSpace(options.Secret), "JWT secret is required.")
|
||||
.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<IServerConfigRepository, ServerConfigRepository>();
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -8,16 +8,25 @@ public class ConfigService(IServerConfigRepository repository) : IConfigService
|
||||
{
|
||||
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,
|
||||
T? defaultValue = default,
|
||||
T defaultValue,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var config = await repository.GetAsync(key, ct);
|
||||
if (config == null)
|
||||
return defaultValue;
|
||||
|
||||
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions);
|
||||
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions) ?? defaultValue;
|
||||
}
|
||||
|
||||
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)
|
||||
claims.Add(new Claim(ClaimTypes.Role, "moderator"));
|
||||
claims.Add(new Claim("role", "moderator"));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: jwtOptions.Issuer,
|
||||
|
||||
Reference in New Issue
Block a user