Move projects to src; add Neutrino secret
This commit is contained in:
21
src/API/API.csproj
Normal file
21
src/API/API.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>cfcf35c1-8c26-473a-a8e9-44ba009adea8</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RecNet.Application\RecNet.Application.csproj" />
|
||||
<ProjectReference Include="..\RecNet.Infrastructure\RecNet.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\RecNet.ServiceDefaults\RecNet.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
7
src/API/Configuration/NeutrinoOptions.cs
Normal file
7
src/API/Configuration/NeutrinoOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace API.Configuration;
|
||||
|
||||
public class NeutrinoOptions
|
||||
{
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
public bool ValidateSecret { get; set; } = false;
|
||||
}
|
||||
6
src/API/Configuration/RecNetOptions.cs
Normal file
6
src/API/Configuration/RecNetOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace API.Configuration;
|
||||
|
||||
public class RecNetOptions
|
||||
{
|
||||
public bool UseForwardedHeaders { get; set; }
|
||||
}
|
||||
25
src/API/Contracts/Neutrino/Enums/AuthenticationResultCode.cs
Normal file
25
src/API/Contracts/Neutrino/Enums/AuthenticationResultCode.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace API.Contracts.Neutrino.Enums;
|
||||
|
||||
public enum AuthenticationResultCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that authentication is incomplete and only the associated data is returned.
|
||||
/// </summary>
|
||||
/// <remarks>This value is typically used when launching the game with no account, in which data to authenticate is not provided.</remarks>
|
||||
AuthenticationIncomplete,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that authentication was successful.
|
||||
/// </summary>
|
||||
AuthenticationSuccessful,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that authentication failed due to incorrect credentials.
|
||||
/// </summary>
|
||||
AuthenticationFailedWrongCredentials,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the parameters provided to the operation are invalid.
|
||||
/// </summary>
|
||||
InvalidParameters
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace API.Contracts.Neutrino.Requests;
|
||||
|
||||
public class NeutrinoAuthenticateRequest
|
||||
{
|
||||
[JsonPropertyName("profileId")]
|
||||
public Guid ProfileId;
|
||||
|
||||
[JsonPropertyName("accessToken")]
|
||||
public required string AccessToken;
|
||||
|
||||
[JsonPropertyName("appVersion")]
|
||||
public required string AppVersion;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using API.Contracts.Neutrino.Enums;
|
||||
|
||||
namespace API.Contracts.Neutrino.Responses;
|
||||
|
||||
// {"ResultCode":1,"UserId":"their user id","Nickname":"splotybean"}
|
||||
public class NeutrinoAuthenticateResponse : NeutrinoResultCodeResponse
|
||||
{
|
||||
public static NeutrinoAuthenticateResponse Success(string userId, string nickname) => new()
|
||||
{
|
||||
ResultCode = (byte)AuthenticationResultCode.AuthenticationSuccessful,
|
||||
UserId = userId,
|
||||
Nickname = nickname
|
||||
};
|
||||
|
||||
public static NeutrinoAuthenticateResponse Failure(AuthenticationResultCode error) => new()
|
||||
{
|
||||
ResultCode = (byte)error
|
||||
};
|
||||
|
||||
[JsonPropertyName(name: "UserId")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
[JsonPropertyName(name: "Nickname")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Nickname { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace API.Contracts.Neutrino.Responses;
|
||||
|
||||
public class NeutrinoResultCodeResponse
|
||||
{
|
||||
public static NeutrinoResultCodeResponse Success() => new NeutrinoResultCodeResponse
|
||||
{
|
||||
ResultCode = 0
|
||||
};
|
||||
|
||||
public static NeutrinoResultCodeResponse Failure(byte resultCode, string? message = null)
|
||||
{
|
||||
return new NeutrinoResultCodeResponse
|
||||
{
|
||||
Message = message,
|
||||
ResultCode = resultCode
|
||||
};
|
||||
}
|
||||
|
||||
[JsonPropertyName(name: "Data")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Data { get; set; }
|
||||
|
||||
[JsonPropertyName(name: "Message")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[JsonPropertyName(name: "ResultCode")]
|
||||
public byte ResultCode { get; set; }
|
||||
}
|
||||
22
src/API/Contracts/Profiles/Requests/LoginRequest.cs
Normal file
22
src/API/Contracts/Profiles/Requests/LoginRequest.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using RecNet.Domain.Common;
|
||||
|
||||
namespace API.Contracts.Profiles.Requests;
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
[Required]
|
||||
public required string AppVersion { get; set; }
|
||||
|
||||
[Required, StringLength(128, MinimumLength = 1)]
|
||||
public required string DeviceId { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string PlatformAuthentication { get; set; }
|
||||
|
||||
[Required, StringLength(50, MinimumLength = 1)]
|
||||
public required string PlatformId { get; set; }
|
||||
|
||||
[EnumDataType(typeof(PlatformType))]
|
||||
public PlatformType PlatformType { get; set; }
|
||||
}
|
||||
16
src/API/Contracts/Profiles/Responses/LoginResponse.cs
Normal file
16
src/API/Contracts/Profiles/Responses/LoginResponse.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using RecNet.Application.Profiles;
|
||||
|
||||
namespace API.Contracts.Profiles.Responses;
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
[JsonPropertyName("Profile")]
|
||||
public required ProfileDTO Profile { get; set; }
|
||||
|
||||
[JsonPropertyName("AccessToken")]
|
||||
public required string AccessToken { get; set; }
|
||||
|
||||
[JsonPropertyName("ExpiresIn")]
|
||||
public required int ExpiresIn { get; set; }
|
||||
}
|
||||
17
src/API/Controllers/Config/V1/ConfigController.cs
Normal file
17
src/API/Controllers/Config/V1/ConfigController.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
|
||||
namespace API.Controllers.Config.V1;
|
||||
|
||||
[Route("api/[controller]/v1")]
|
||||
[ApiController]
|
||||
public class ConfigController(IConfigService configService) : ControllerBase
|
||||
{
|
||||
[HttpGet("motd")]
|
||||
public async Task<ActionResult<string>> GetMotd(CancellationToken ct = default)
|
||||
{
|
||||
var motd = await configService.GetAsync("Config:MOTD", "Ten Whole Years!", ct);
|
||||
|
||||
return Ok(motd);
|
||||
}
|
||||
}
|
||||
75
src/API/Controllers/Neutrino/NeutrinoController.cs
Normal file
75
src/API/Controllers/Neutrino/NeutrinoController.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Text.Json;
|
||||
using API.Configuration;
|
||||
using API.Contracts.Neutrino.Enums;
|
||||
using API.Contracts.Neutrino.Requests;
|
||||
using API.Contracts.Neutrino.Responses;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RecNet.Application.Neutrino;
|
||||
|
||||
namespace API.Controllers.Neutrino;
|
||||
|
||||
[Route("[controller]")]
|
||||
[ApiController]
|
||||
public class NeutrinoController(
|
||||
INeutrinoAuthorizationService authorizationService,
|
||||
IOptions<NeutrinoOptions> options) : ControllerBase
|
||||
{
|
||||
private readonly NeutrinoOptions _options = options.Value;
|
||||
|
||||
// Photon sends the request with Content-Type: text/plain instead of application/json.
|
||||
[Route("authorize")]
|
||||
public async Task<ActionResult<NeutrinoAuthenticateResponse>> AuthorizeNeutrinoAsync(
|
||||
[FromQuery(Name = "secret")] string? secret = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (_options.ValidateSecret && secret != _options.Secret)
|
||||
return Forbid();
|
||||
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
string rawText = await reader.ReadToEndAsync(ct);
|
||||
|
||||
NeutrinoAuthenticateRequest? request;
|
||||
|
||||
try
|
||||
{
|
||||
request = JsonSerializer.Deserialize<NeutrinoAuthenticateRequest>(rawText, new JsonSerializerOptions
|
||||
{
|
||||
IncludeFields = true,
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Ok(
|
||||
NeutrinoAuthenticateResponse.Failure(
|
||||
AuthenticationResultCode.InvalidParameters));
|
||||
}
|
||||
|
||||
if (request is null)
|
||||
return Ok(
|
||||
NeutrinoAuthenticateResponse.Failure(
|
||||
AuthenticationResultCode.InvalidParameters));
|
||||
|
||||
var result = await authorizationService.AuthorizeAsync(
|
||||
new AuthorizeNeutrinoCommand(
|
||||
request.ProfileId,
|
||||
request.AccessToken),
|
||||
ct);
|
||||
|
||||
if (!result.Succeeded)
|
||||
return Ok(
|
||||
NeutrinoAuthenticateResponse.Failure(
|
||||
MapFailure(result.Failure)));
|
||||
|
||||
return Ok(
|
||||
NeutrinoAuthenticateResponse.Success(
|
||||
result.UserId!,
|
||||
result.Nickname!));
|
||||
}
|
||||
|
||||
private static AuthenticationResultCode MapFailure(NeutrinoAuthorizationFailure? failure)
|
||||
=> failure == NeutrinoAuthorizationFailure.InvalidParameters
|
||||
? AuthenticationResultCode.InvalidParameters
|
||||
: AuthenticationResultCode.AuthenticationFailedWrongCredentials;
|
||||
}
|
||||
53
src/API/Controllers/Profiles/V1/ProfilesController.cs
Normal file
53
src/API/Controllers/Profiles/V1/ProfilesController.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using API.Contracts.Profiles.Responses;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RecNet.Application.Profiles;
|
||||
using LoginRequest = API.Contracts.Profiles.Requests.LoginRequest;
|
||||
|
||||
namespace API.Controllers.Profiles.V1;
|
||||
|
||||
[Route("api/[controller]/v1")]
|
||||
[ApiController]
|
||||
public class ProfilesController(IProfileService profileService) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<ProfileDTO>> GetProfile(
|
||||
Guid id,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var profile = await profileService.GetProfileAsync(id, ct);
|
||||
if (profile == null)
|
||||
return NotFound();
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
[HttpGet("bulk")]
|
||||
public async Task<IReadOnlyList<ProfileDTO>> GetBulkProfiles(
|
||||
[FromQuery(Name = "id")] List<Guid> ids,
|
||||
CancellationToken ct)
|
||||
=> await profileService.GetProfilesAsync(ids, ct);
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<LoginResponse>> Login(
|
||||
[FromBody] LoginRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = await profileService.LoginAsync(
|
||||
new LoginProfileCommand(
|
||||
request.AppVersion,
|
||||
request.DeviceId,
|
||||
request.PlatformType,
|
||||
request.PlatformId,
|
||||
request.PlatformAuthentication
|
||||
), ct);
|
||||
if (!result.Succeeded)
|
||||
return BadRequest(result.Message);
|
||||
|
||||
return new LoginResponse
|
||||
{
|
||||
Profile = result.Profile!,
|
||||
AccessToken = result.AccessToken!,
|
||||
ExpiresIn = result.ExpiresIn
|
||||
};
|
||||
}
|
||||
}
|
||||
79
src/API/Program.cs
Normal file
79
src/API/Program.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Text;
|
||||
using API.Configuration;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using RecNet.Application;
|
||||
using RecNet.Infrastructure;
|
||||
using RecNet.Infrastructure.Services.Tokens;
|
||||
using RecNet.ServiceDefaults;
|
||||
|
||||
namespace API;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services
|
||||
.AddOptions<NeutrinoOptions>()
|
||||
.Bind(builder.Configuration.GetSection("Neutrino"))
|
||||
.ValidateOnStart();
|
||||
|
||||
var recNetOptions = builder.Configuration.GetSection("RecNet").Get<RecNetOptions>()
|
||||
?? new RecNetOptions();
|
||||
|
||||
var jwtOptions = builder.Configuration.GetSection("Jwt").Get<JwtOptions>()
|
||||
?? new JwtOptions();
|
||||
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
ForwardedHeaders.XForwardedFor |
|
||||
ForwardedHeaders.XForwardedHost |
|
||||
ForwardedHeaders.XForwardedProto;
|
||||
options.KnownIPNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
builder.Services.AddApplication();
|
||||
builder.AddInfrastructure();
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Secret)),
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jwtOptions.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtOptions.Audience,
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (recNetOptions.UseForwardedHeaders)
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
23
src/API/Properties/launchSettings.json
Normal file
23
src/API/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5155",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7218;http://localhost:5155",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/API/appsettings.Development.json
Normal file
8
src/API/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/API/appsettings.json
Normal file
9
src/API/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
25
src/AppHost/AppHost.cs
Normal file
25
src/AppHost/AppHost.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
var postgresPassword = builder.AddParameter("postgres-password", secret: true);
|
||||
|
||||
// Infra
|
||||
var postgres = builder.AddPostgres("postgres")
|
||||
.WithDataVolume("recnet-postgres-data")
|
||||
.WithLifetime(ContainerLifetime.Persistent)
|
||||
.WithPassword(postgresPassword)
|
||||
.WithHostPort(5432)
|
||||
.WithPgAdmin();
|
||||
|
||||
var db = postgres.AddDatabase("recnet");
|
||||
|
||||
// Services
|
||||
var migrationService = builder.AddProject<Projects.RecNet_MigrationService>("migrationservice")
|
||||
.WithReference(db)
|
||||
.WaitFor(db);
|
||||
|
||||
builder.AddProject<Projects.API>("api")
|
||||
.WithReference(db)
|
||||
.WaitFor(db)
|
||||
.WaitForCompletion(migrationService);
|
||||
|
||||
builder.Build().Run();
|
||||
20
src/AppHost/AppHost.csproj
Normal file
20
src/AppHost/AppHost.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Aspire.AppHost.Sdk/13.4.5">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>f7385039-092b-4272-a61a-69c6a3d0d618</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.4.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\API\API.csproj" />
|
||||
<ProjectReference Include="..\RecNet.MigrationService\RecNet.MigrationService.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
29
src/AppHost/Properties/launchSettings.json
Normal file
29
src/AppHost/Properties/launchSettings.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:17063;http://localhost:15151",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21097",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22143"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:15151",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19205",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20044"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/AppHost/appsettings.Development.json
Normal file
8
src/AppHost/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/AppHost/appsettings.json
Normal file
9
src/AppHost/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Aspire.Hosting.Dcp": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/AppHost/aspire.config.json
Normal file
5
src/AppHost/aspire.config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"appHost": {
|
||||
"path": "AppHost.csproj"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RecNet.Application.Common.Interfaces;
|
||||
|
||||
public interface IConfigService
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
10
src/RecNet.Application/Common/Interfaces/ITokenService.cs
Normal file
10
src/RecNet.Application/Common/Interfaces/ITokenService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using RecNet.Application.Common.Tokens;
|
||||
using RecNet.Domain.Profiles;
|
||||
|
||||
namespace RecNet.Application.Common.Interfaces;
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
TokenResult GenerateProfileToken(Profile profile);
|
||||
TokenVerifyResult VerifyToken(string token);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using AutoMapper;
|
||||
using RecNet.Application.Profiles;
|
||||
|
||||
namespace RecNet.Application.Common.Mapping;
|
||||
|
||||
public class ApplicationMappingProfile : Profile
|
||||
{
|
||||
public ApplicationMappingProfile()
|
||||
{
|
||||
CreateMap<Domain.Profiles.Profile, ProfileDTO>();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace RecNet.Application.Common.Security;
|
||||
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
public static Guid GetProfileId(this ClaimsPrincipal user)
|
||||
{
|
||||
var value =
|
||||
user.FindFirst("sub")?.Value ??
|
||||
user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (!Guid.TryParse(value, out var profileId))
|
||||
throw new UnauthorizedAccessException(
|
||||
"Account id claim is missing or invalid.");
|
||||
|
||||
return profileId;
|
||||
}
|
||||
}
|
||||
21
src/RecNet.Application/Common/Steam/SteamAuthResult.cs
Normal file
21
src/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);
|
||||
}
|
||||
5
src/RecNet.Application/Common/Tokens/TokenResult.cs
Normal file
5
src/RecNet.Application/Common/Tokens/TokenResult.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace RecNet.Application.Common.Tokens;
|
||||
|
||||
public sealed record TokenResult(
|
||||
string AccessToken,
|
||||
int ExpiresIn);
|
||||
12
src/RecNet.Application/Common/Tokens/TokenVerifyResult.cs
Normal file
12
src/RecNet.Application/Common/Tokens/TokenVerifyResult.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace RecNet.Application.Common.Tokens;
|
||||
|
||||
public sealed record TokenVerifyResult(
|
||||
bool Succeeded,
|
||||
Guid? ProfileId)
|
||||
{
|
||||
public static TokenVerifyResult Success(Guid profileId)
|
||||
=> new(true, profileId);
|
||||
|
||||
public static TokenVerifyResult Failure()
|
||||
=> new(false, null);
|
||||
}
|
||||
20
src/RecNet.Application/DependencyInjection.cs
Normal file
20
src/RecNet.Application/DependencyInjection.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RecNet.Application.Neutrino;
|
||||
using RecNet.Application.Profiles;
|
||||
|
||||
namespace RecNet.Application;
|
||||
|
||||
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>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace RecNet.Application.Neutrino;
|
||||
|
||||
public sealed record AuthorizeNeutrinoCommand(
|
||||
Guid ProfileId,
|
||||
string AccessToken);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RecNet.Application.Neutrino;
|
||||
|
||||
public interface INeutrinoAuthorizationService
|
||||
{
|
||||
Task<NeutrinoAuthorizationResult> AuthorizeAsync(
|
||||
AuthorizeNeutrinoCommand command,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace RecNet.Application.Neutrino;
|
||||
|
||||
public enum NeutrinoAuthorizationFailure
|
||||
{
|
||||
InvalidParameters,
|
||||
AuthenticationFailed
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace RecNet.Application.Neutrino;
|
||||
|
||||
public sealed record NeutrinoAuthorizationResult(
|
||||
bool Succeeded,
|
||||
NeutrinoAuthorizationFailure? Failure,
|
||||
string? UserId,
|
||||
string? Nickname)
|
||||
{
|
||||
public static NeutrinoAuthorizationResult Success(Guid userId, string nickname)
|
||||
=> new(true, null, userId.ToString(), nickname);
|
||||
|
||||
public static NeutrinoAuthorizationResult InvalidParameters()
|
||||
=> new(false, NeutrinoAuthorizationFailure.InvalidParameters, null, null);
|
||||
|
||||
public static NeutrinoAuthorizationResult AuthenticationFailed()
|
||||
=> new(false, NeutrinoAuthorizationFailure.AuthenticationFailed, null, null);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
using RecNet.Domain.Profiles;
|
||||
|
||||
namespace RecNet.Application.Neutrino;
|
||||
|
||||
public class NeutrinoAuthorizationService(
|
||||
IProfileRepository profileRepository,
|
||||
ITokenService tokenService) : INeutrinoAuthorizationService
|
||||
{
|
||||
public async Task<NeutrinoAuthorizationResult> AuthorizeAsync(
|
||||
AuthorizeNeutrinoCommand command,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (command.ProfileId == Guid.Empty || string.IsNullOrWhiteSpace(command.AccessToken))
|
||||
return NeutrinoAuthorizationResult.InvalidParameters();
|
||||
|
||||
var tokenVerifyResult = tokenService.VerifyToken(command.AccessToken);
|
||||
if (!tokenVerifyResult.Succeeded || tokenVerifyResult.ProfileId is null)
|
||||
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
||||
|
||||
if (tokenVerifyResult.ProfileId.Value != command.ProfileId)
|
||||
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
||||
|
||||
var profile = await profileRepository.GetByIdAsync(command.ProfileId, ct);
|
||||
if (profile is null || profile.IsBanned)
|
||||
return NeutrinoAuthorizationResult.AuthenticationFailed();
|
||||
|
||||
return NeutrinoAuthorizationResult.Success(profile.ProfileId, profile.Name);
|
||||
}
|
||||
}
|
||||
8
src/RecNet.Application/Profiles/IProfileService.cs
Normal file
8
src/RecNet.Application/Profiles/IProfileService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace RecNet.Application.Profiles;
|
||||
|
||||
public interface IProfileService
|
||||
{
|
||||
Task<ProfileDTO?> GetProfileAsync(Guid profileId, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<ProfileDTO>> GetProfilesAsync(List<Guid> profileIds, CancellationToken ct = default);
|
||||
Task<LoginProfileResult> LoginAsync(LoginProfileCommand command, CancellationToken ct = default);
|
||||
}
|
||||
10
src/RecNet.Application/Profiles/LoginProfileCommand.cs
Normal file
10
src/RecNet.Application/Profiles/LoginProfileCommand.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using RecNet.Domain.Common;
|
||||
|
||||
namespace RecNet.Application.Profiles;
|
||||
|
||||
public sealed record LoginProfileCommand(
|
||||
string AppVersion,
|
||||
string DeviceId,
|
||||
PlatformType PlatformType,
|
||||
string PlatformId,
|
||||
string PlatformAuthentication);
|
||||
33
src/RecNet.Application/Profiles/LoginProfileResult.cs
Normal file
33
src/RecNet.Application/Profiles/LoginProfileResult.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace RecNet.Application.Profiles;
|
||||
|
||||
|
||||
|
||||
public class LoginProfileResult
|
||||
{
|
||||
public bool Succeeded { get; set; }
|
||||
public string? Message { get; set; }
|
||||
|
||||
public ProfileDTO? Profile { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
public static LoginProfileResult Success(ProfileDTO profile, string accessToken, int expiresIn)
|
||||
{
|
||||
return new LoginProfileResult
|
||||
{
|
||||
Succeeded = true,
|
||||
Profile = profile,
|
||||
AccessToken = accessToken,
|
||||
ExpiresIn = expiresIn
|
||||
};
|
||||
}
|
||||
|
||||
public static LoginProfileResult Fail(string? message = null)
|
||||
{
|
||||
return new LoginProfileResult
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
17
src/RecNet.Application/Profiles/NameGenerator.cs
Normal file
17
src/RecNet.Application/Profiles/NameGenerator.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
using RecNet.Domain.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}";
|
||||
}
|
||||
}
|
||||
12
src/RecNet.Application/Profiles/ProfileDto.cs
Normal file
12
src/RecNet.Application/Profiles/ProfileDto.cs
Normal file
@@ -0,0 +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; }
|
||||
}
|
||||
95
src/RecNet.Application/Profiles/ProfileService.cs
Normal file
95
src/RecNet.Application/Profiles/ProfileService.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using AutoMapper;
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
using RecNet.Domain.GameVersions;
|
||||
using RecNet.Domain.Profiles;
|
||||
using ProfileEntity = RecNet.Domain.Profiles.Profile;
|
||||
|
||||
namespace RecNet.Application.Profiles;
|
||||
|
||||
public class ProfileService(
|
||||
IEnumerable<IPlatformAuthValidator> validators,
|
||||
IGameVersionRepository gameVersionRepository,
|
||||
IProfileRepository profileRepository,
|
||||
IConfigService configService,
|
||||
NameGenerator nameGenerator,
|
||||
ITokenService tokenService,
|
||||
IMapper mapper) : IProfileService
|
||||
{
|
||||
public async Task<ProfileDTO?> GetProfileAsync(Guid profileId, CancellationToken ct = default)
|
||||
{
|
||||
var profile = await profileRepository.GetByIdAsync(profileId, ct);
|
||||
|
||||
return profile is null
|
||||
? null
|
||||
: mapper.Map<ProfileDTO>(profile);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ProfileDTO>> GetProfilesAsync(
|
||||
List<Guid> profileIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var profiles = await profileRepository.GetByIdsAsync(profileIds, ct);
|
||||
|
||||
return mapper.Map<List<ProfileDTO>>(profiles);
|
||||
}
|
||||
|
||||
public async Task<LoginProfileResult> LoginAsync(
|
||||
LoginProfileCommand command,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// 1. Validate Game Version
|
||||
if (string.IsNullOrWhiteSpace(command.AppVersion))
|
||||
return LoginProfileResult.Fail("App version is required.");
|
||||
|
||||
var isValidGameVersion = await gameVersionRepository.IsValidAsync(command.AppVersion, ct);
|
||||
if (!isValidGameVersion)
|
||||
return LoginProfileResult.Fail("Invalid app version.");
|
||||
|
||||
// 2. Validate Platform Authentication
|
||||
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}");
|
||||
|
||||
// 3. Get or Create Profile
|
||||
var profile = await profileRepository.GetByPlatform(
|
||||
command.PlatformType,
|
||||
command.PlatformId,
|
||||
ct);
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
var name = await nameGenerator.GenerateAsync(ct);
|
||||
|
||||
profile = ProfileEntity.Create(
|
||||
name,
|
||||
command.PlatformType,
|
||||
command.PlatformId);
|
||||
|
||||
await profileRepository.AddAsync(profile, ct);
|
||||
}
|
||||
|
||||
if (profile.IsBanned)
|
||||
return LoginProfileResult.Fail("Profile is banned");
|
||||
|
||||
profile.RecordSuccessfulLogin(command.DeviceId, auth.Name);
|
||||
|
||||
await profileRepository.SaveChangesAsync(ct);
|
||||
|
||||
var token = tokenService.GenerateProfileToken(profile);
|
||||
|
||||
return LoginProfileResult.Success(
|
||||
profile: mapper.Map<ProfileDTO>(profile),
|
||||
accessToken: token.AccessToken,
|
||||
expiresIn: token.ExpiresIn
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/RecNet.Application/RecNet.Application.csproj
Normal file
21
src/RecNet.Application/RecNet.Application.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RecNet.Domain\RecNet.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Authentication\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
7
src/RecNet.Domain/Common/PlatformType.cs
Normal file
7
src/RecNet.Domain/Common/PlatformType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace RecNet.Domain.Common;
|
||||
|
||||
public enum PlatformType
|
||||
{
|
||||
Steamworks = 0,
|
||||
Meta = 1,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RecNet.Domain.Configuration;
|
||||
|
||||
public interface IServerConfigRepository
|
||||
{
|
||||
Task<ServerConfig?> GetAsync(string key, CancellationToken ct = default);
|
||||
Task SetAsync(string key, string value, CancellationToken ct = default);
|
||||
Task SaveChangesAsync(CancellationToken ct = default);
|
||||
}
|
||||
24
src/RecNet.Domain/Configuration/ServerConfig.cs
Normal file
24
src/RecNet.Domain/Configuration/ServerConfig.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using RecNet.Domain.Exceptions;
|
||||
|
||||
namespace RecNet.Domain.Configuration;
|
||||
|
||||
public class ServerConfig
|
||||
{
|
||||
private ServerConfig(string key, string value)
|
||||
{
|
||||
Key = RequireValue(key, nameof(key));
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Key { get; private set; }
|
||||
public string Value { get; private set; }
|
||||
|
||||
public static ServerConfig Create(string key, string value)
|
||||
=> new(key, value);
|
||||
|
||||
public void SetValue(string value)
|
||||
=> Value = value;
|
||||
|
||||
private static string RequireValue(string value, string parameterName)
|
||||
=> !string.IsNullOrWhiteSpace(value) ? value : throw new DomainException($"{parameterName} is required.");
|
||||
}
|
||||
15
src/RecNet.Domain/Exceptions/DomainException.cs
Normal file
15
src/RecNet.Domain/Exceptions/DomainException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace RecNet.Domain.Exceptions;
|
||||
|
||||
public class DomainException : Exception
|
||||
{
|
||||
public DomainException()
|
||||
{ }
|
||||
|
||||
public DomainException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public DomainException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
31
src/RecNet.Domain/GameVersions/GameVersion.cs
Normal file
31
src/RecNet.Domain/GameVersions/GameVersion.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using RecNet.Domain.Exceptions;
|
||||
|
||||
namespace RecNet.Domain.GameVersions;
|
||||
|
||||
public class GameVersion
|
||||
{
|
||||
private GameVersion()
|
||||
{
|
||||
}
|
||||
|
||||
private GameVersion(string version, bool isValid)
|
||||
{
|
||||
Version = RequireValue(version, nameof(version));
|
||||
IsValid = isValid;
|
||||
}
|
||||
|
||||
public string Version { get; private set; } = string.Empty;
|
||||
public bool IsValid { get; private set; }
|
||||
|
||||
public static GameVersion Create(string version, bool isValid)
|
||||
=> new(version, isValid);
|
||||
|
||||
public void MarkValid()
|
||||
=> IsValid = true;
|
||||
|
||||
public void MarkInvalid()
|
||||
=> IsValid = false;
|
||||
|
||||
private static string RequireValue(string value, string parameterName)
|
||||
=> !string.IsNullOrWhiteSpace(value) ? value : throw new DomainException($"{parameterName} is required.");
|
||||
}
|
||||
12
src/RecNet.Domain/GameVersions/IGameVersionRepository.cs
Normal file
12
src/RecNet.Domain/GameVersions/IGameVersionRepository.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace RecNet.Domain.GameVersions;
|
||||
|
||||
public interface IGameVersionRepository
|
||||
{
|
||||
Task<GameVersion?> GetAsync(string version, CancellationToken ct = default);
|
||||
|
||||
async Task<bool> IsValidAsync(string version, CancellationToken ct = default)
|
||||
{
|
||||
var gameVersion = await GetAsync(version, ct);
|
||||
return gameVersion?.IsValid == true;
|
||||
}
|
||||
}
|
||||
15
src/RecNet.Domain/Profiles/IProfileRepository.cs
Normal file
15
src/RecNet.Domain/Profiles/IProfileRepository.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using RecNet.Domain.Common;
|
||||
|
||||
namespace RecNet.Domain.Profiles;
|
||||
|
||||
public interface IProfileRepository
|
||||
{
|
||||
Task<Profile?> GetByIdAsync(Guid profileId, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<Profile>> GetByIdsAsync(List<Guid> profileIds, CancellationToken ct = default);
|
||||
|
||||
Task<Profile?> GetByPlatform(PlatformType platform, string platformId, CancellationToken ct = default);
|
||||
|
||||
Task AddAsync(Profile profile, CancellationToken ct = default);
|
||||
|
||||
Task SaveChangesAsync(CancellationToken ct = default);
|
||||
}
|
||||
288
src/RecNet.Domain/Profiles/Names/DefaultNameGenConfig.cs
Normal file
288
src/RecNet.Domain/Profiles/Names/DefaultNameGenConfig.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
namespace RecNet.Domain.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
src/RecNet.Domain/Profiles/Names/NameGenConfig.cs
Normal file
7
src/RecNet.Domain/Profiles/Names/NameGenConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace RecNet.Domain.Profiles.Names;
|
||||
|
||||
public class NameGenConfig
|
||||
{
|
||||
public required List<string> Adjectives { get; set; }
|
||||
public required List<string> Nouns { get; set; }
|
||||
}
|
||||
86
src/RecNet.Domain/Profiles/Profile.cs
Normal file
86
src/RecNet.Domain/Profiles/Profile.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using RecNet.Domain.Common;
|
||||
using RecNet.Domain.Exceptions;
|
||||
|
||||
namespace RecNet.Domain.Profiles;
|
||||
|
||||
public class Profile
|
||||
{
|
||||
public const int MinNameLength = 3;
|
||||
public const int MaxNameLength = 32;
|
||||
|
||||
private Profile(
|
||||
string name,
|
||||
PlatformType platform,
|
||||
string platformId)
|
||||
{
|
||||
SetName(name);
|
||||
|
||||
Platform = platform;
|
||||
PlatformId = RequireValue(platformId, nameof(platformId));
|
||||
|
||||
CreatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public Guid ProfileId { get; } = Guid.NewGuid();
|
||||
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
public PlatformType Platform { get; private set; }
|
||||
public string PlatformId { get; private set; }
|
||||
public List<string> DeviceIds { get; private set; } = [];
|
||||
|
||||
// EZ
|
||||
public bool IsBanned { get; private set; }
|
||||
public bool IsModerator { get; private set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
public static Profile Create(
|
||||
string name,
|
||||
PlatformType platform,
|
||||
string platformId)
|
||||
=> new(name, platform, platformId);
|
||||
|
||||
public void SetName(string name)
|
||||
{
|
||||
var requiredName = RequireValue(name, nameof(name));
|
||||
|
||||
if (requiredName.Length is < MinNameLength or > MaxNameLength)
|
||||
throw new DomainException($"Username must be between {MinNameLength} and {MaxNameLength} characters long.");
|
||||
|
||||
Name = requiredName;
|
||||
}
|
||||
|
||||
public void AddDeviceId(string deviceId)
|
||||
{
|
||||
deviceId = RequireValue(deviceId, nameof(deviceId));
|
||||
|
||||
if (DeviceIds.Contains(deviceId))
|
||||
return;
|
||||
|
||||
DeviceIds.Add(deviceId);
|
||||
}
|
||||
|
||||
public void RecordSuccessfulLogin(string deviceId, string? platformName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(platformName) && Name != platformName)
|
||||
SetName(platformName);
|
||||
|
||||
AddDeviceId(deviceId);
|
||||
}
|
||||
|
||||
public void Ban()
|
||||
=> IsBanned = true;
|
||||
|
||||
public void Unban()
|
||||
=> IsBanned = false;
|
||||
|
||||
public void GrantModerator()
|
||||
=> IsModerator = true;
|
||||
|
||||
public void RevokeModerator()
|
||||
=> IsModerator = false;
|
||||
|
||||
private static string RequireValue(string value, string parameterName)
|
||||
=> !string.IsNullOrWhiteSpace(value) ? value : throw new DomainException($"{parameterName} is required.");
|
||||
}
|
||||
9
src/RecNet.Domain/RecNet.Domain.csproj
Normal file
9
src/RecNet.Domain/RecNet.Domain.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
50
src/RecNet.Infrastructure/DependencyInjection.cs
Normal file
50
src/RecNet.Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
using RecNet.Domain.Configuration;
|
||||
using RecNet.Domain.GameVersions;
|
||||
using RecNet.Domain.Profiles;
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
using RecNet.Infrastructure.Persistence.Repositories;
|
||||
using RecNet.Infrastructure.Services.Configuration;
|
||||
using RecNet.Infrastructure.Services.Meta;
|
||||
using RecNet.Infrastructure.Services.Steam;
|
||||
using RecNet.Infrastructure.Services.Tokens;
|
||||
|
||||
namespace RecNet.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static TBuilder AddInfrastructure<TBuilder>(this TBuilder builder)
|
||||
where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.AddNpgsqlDbContext<DatabaseContext>("recnet");
|
||||
|
||||
builder.Services
|
||||
.AddOptions<JwtOptions>()
|
||||
.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<IPlatformAuthValidator, MetaAuthValidator>();
|
||||
|
||||
builder.Services.AddScoped<IGameVersionRepository, GameVersionRepository>();
|
||||
builder.Services.AddScoped<IProfileRepository, ProfileRepository>();
|
||||
builder.Services.AddScoped<IServerConfigRepository, ServerConfigRepository>();
|
||||
|
||||
builder.Services.AddScoped<IConfigService, ConfigService>();
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using RecNet.Domain.GameVersions;
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class GameVersionConfiguration : IEntityTypeConfiguration<GameVersion>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<GameVersion> builder)
|
||||
{
|
||||
builder.HasKey(x => x.Version);
|
||||
|
||||
builder.Property(x => x.Version)
|
||||
.HasMaxLength(32)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.IsValid)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using RecNet.Domain.Profiles;
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class ProfileConfiguration : IEntityTypeConfiguration<Profile>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Profile> builder)
|
||||
{
|
||||
builder.HasKey(x => x.ProfileId);
|
||||
|
||||
builder.Property(x => x.Name)
|
||||
.HasMaxLength(Profile.MaxNameLength)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.Platform)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.PlatformId)
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.DeviceIds)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
builder.Property(x => x.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasIndex(x => new { x.Platform, x.PlatformId })
|
||||
.IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using RecNet.Domain.Configuration;
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class ServerConfigConfiguration : IEntityTypeConfiguration<ServerConfig>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ServerConfig> builder)
|
||||
{
|
||||
builder.HasKey(x => x.Key);
|
||||
|
||||
builder.Property(x => x.Key)
|
||||
.HasMaxLength(128)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(x => x.Value)
|
||||
.HasColumnType("jsonb")
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
18
src/RecNet.Infrastructure/Persistence/DatabaseContext.cs
Normal file
18
src/RecNet.Infrastructure/Persistence/DatabaseContext.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RecNet.Domain.Configuration;
|
||||
using RecNet.Domain.GameVersions;
|
||||
using RecNet.Domain.Profiles;
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence;
|
||||
|
||||
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Profile> Profiles => Set<Profile>();
|
||||
public DbSet<GameVersion> GameVersions => Set<GameVersion>();
|
||||
public DbSet<ServerConfig> ServerConfigs => Set<ServerConfig>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(DatabaseContext).Assembly);
|
||||
}
|
||||
}
|
||||
66
src/RecNet.Infrastructure/Persistence/Migrations/20260618180145_Initial.Designer.cs
generated
Normal file
66
src/RecNet.Infrastructure/Persistence/Migrations/20260618180145_Initial.Designer.cs
generated
Normal file
@@ -0,0 +1,66 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20260618180145_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Entities.Profiles.Profile", b =>
|
||||
{
|
||||
b.Property<Guid>("ProfileId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<string>("DeviceIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("PlatformId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("ProfileId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Profiles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Profiles",
|
||||
columns: table => new
|
||||
{
|
||||
ProfileId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Platform = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
PlatformId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
DeviceIds = table.Column<string>(type: "jsonb", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Profiles", x => x.ProfileId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Profiles_Platform_PlatformId",
|
||||
table: "Profiles",
|
||||
columns: new[] { "Platform", "PlatformId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Profiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/RecNet.Infrastructure/Persistence/Migrations/20260618183916_ServerConfigs.Designer.cs
generated
Normal file
81
src/RecNet.Infrastructure/Persistence/Migrations/20260618183916_ServerConfigs.Designer.cs
generated
Normal file
@@ -0,0 +1,81 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20260618183916_ServerConfigs")]
|
||||
partial class ServerConfigs
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Entities.Configuration.ServerConfig", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Entities.Profiles.Profile", b =>
|
||||
{
|
||||
b.Property<Guid>("ProfileId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<string>("DeviceIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("PlatformId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("ProfileId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Profiles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ServerConfigs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServerConfigs",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Value = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServerConfigs", x => x.Key);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServerConfigs");
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/RecNet.Infrastructure/Persistence/Migrations/20260619025601_ProfileToggles.Designer.cs
generated
Normal file
87
src/RecNet.Infrastructure/Persistence/Migrations/20260619025601_ProfileToggles.Designer.cs
generated
Normal file
@@ -0,0 +1,87 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20260619025601_ProfileToggles")]
|
||||
partial class ProfileToggles
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Entities.Configuration.ServerConfig", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Entities.Profiles.Profile", b =>
|
||||
{
|
||||
b.Property<Guid>("ProfileId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<string>("DeviceIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<bool>("IsBanned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("PlatformId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("ProfileId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Profiles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ProfileToggles : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsBanned",
|
||||
table: "Profiles",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsModerator",
|
||||
table: "Profiles",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsBanned",
|
||||
table: "Profiles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsModerator",
|
||||
table: "Profiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/RecNet.Infrastructure/Persistence/Migrations/20260619201140_GameVersion.Designer.cs
generated
Normal file
101
src/RecNet.Infrastructure/Persistence/Migrations/20260619201140_GameVersion.Designer.cs
generated
Normal file
@@ -0,0 +1,101 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20260619201140_GameVersion")]
|
||||
partial class GameVersion
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Configuration.ServerConfig", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.GameVersions.GameVersion", b =>
|
||||
{
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<bool>("IsValid")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Version");
|
||||
|
||||
b.ToTable("GameVersions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Profiles.Profile", b =>
|
||||
{
|
||||
b.Property<Guid>("ProfileId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<string>("DeviceIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<bool>("IsBanned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("PlatformId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("ProfileId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Profiles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class GameVersion : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GameVersions",
|
||||
columns: table => new
|
||||
{
|
||||
Version = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
IsValid = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GameVersions", x => x.Version);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "GameVersions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
partial class DatabaseContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Configuration.ServerConfig", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.GameVersions.GameVersion", b =>
|
||||
{
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<bool>("IsValid")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Version");
|
||||
|
||||
b.ToTable("GameVersions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RecNet.Domain.Profiles.Profile", b =>
|
||||
{
|
||||
b.Property<Guid>("ProfileId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.PrimitiveCollection<string>("DeviceIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<bool>("IsBanned")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsModerator")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Platform")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("PlatformId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("ProfileId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Profiles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RecNet.Domain.GameVersions;
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class GameVersionRepository(DatabaseContext dbContext) : IGameVersionRepository
|
||||
{
|
||||
public Task<GameVersion?> GetAsync(string version, CancellationToken ct = default)
|
||||
=> dbContext.GameVersions
|
||||
.FirstOrDefaultAsync(x => x.Version == version, ct);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RecNet.Domain.Common;
|
||||
using RecNet.Domain.Profiles;
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class ProfileRepository(DatabaseContext dbContext) : IProfileRepository
|
||||
{
|
||||
public Task<Profile?> GetByIdAsync(
|
||||
Guid profileId,
|
||||
CancellationToken ct = default)
|
||||
=> dbContext.Profiles
|
||||
.FirstOrDefaultAsync(x => x.ProfileId == profileId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Profile>> GetByIdsAsync(
|
||||
List<Guid> profileIds,
|
||||
CancellationToken ct = default)
|
||||
=> await dbContext.Profiles
|
||||
.Where(x => profileIds.Contains(x.ProfileId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
public Task<Profile?> GetByPlatform(
|
||||
PlatformType platform,
|
||||
string platformId,
|
||||
CancellationToken ct = default)
|
||||
=> dbContext.Profiles
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.Platform == platform &&
|
||||
x.PlatformId == platformId,
|
||||
ct);
|
||||
|
||||
public async Task AddAsync(
|
||||
Profile profile,
|
||||
CancellationToken ct = default)
|
||||
=> await dbContext.Profiles.AddAsync(profile, ct);
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken ct = default)
|
||||
=> dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using RecNet.Domain.Configuration;
|
||||
|
||||
namespace RecNet.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class ServerConfigRepository(DatabaseContext dbContext) : IServerConfigRepository
|
||||
{
|
||||
public Task<ServerConfig?> GetAsync(string key, CancellationToken ct = default)
|
||||
=> dbContext.ServerConfigs
|
||||
.FirstOrDefaultAsync(x => x.Key == key, ct);
|
||||
|
||||
public async Task SetAsync(string key, string value, CancellationToken ct = default)
|
||||
{
|
||||
var config = await GetAsync(key, ct);
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
config = ServerConfig.Create(key, value);
|
||||
await dbContext.ServerConfigs.AddAsync(config, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
config.SetValue(value);
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken ct = default)
|
||||
=> dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
25
src/RecNet.Infrastructure/RecNet.Infrastructure.csproj
Normal file
25
src/RecNet.Infrastructure/RecNet.Infrastructure.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RecNet.Application\RecNet.Application.csproj" />
|
||||
<ProjectReference Include="..\RecNet.Domain\RecNet.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Persistence\Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Text.Json;
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
using RecNet.Domain.Configuration;
|
||||
|
||||
namespace RecNet.Infrastructure.Services.Configuration;
|
||||
|
||||
public class ConfigService(IServerConfigRepository repository) : IConfigService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
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,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var config = await repository.GetAsync(key, ct);
|
||||
if (config == null)
|
||||
return defaultValue;
|
||||
|
||||
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions) ?? defaultValue;
|
||||
}
|
||||
|
||||
public async Task SetAsync<T>(
|
||||
string key,
|
||||
T value,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value, JsonOptions);
|
||||
|
||||
await repository.SetAsync(key, json, ct);
|
||||
|
||||
await repository.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
15
src/RecNet.Infrastructure/Services/Meta/MetaAuthValidator.cs
Normal file
15
src/RecNet.Infrastructure/Services/Meta/MetaAuthValidator.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
using RecNet.Application.Common.PlatformAuth;
|
||||
using RecNet.Domain.Common;
|
||||
|
||||
namespace RecNet.Infrastructure.Services.Meta;
|
||||
|
||||
public class MetaAuthValidator : IPlatformAuthValidator
|
||||
{
|
||||
public PlatformType PlatformType
|
||||
=> PlatformType.Meta;
|
||||
|
||||
public Task<PlatformAuthResult> ValidateAsync(string platformAuthentication, string platformId,
|
||||
CancellationToken ct = default)
|
||||
=> Task.FromResult(PlatformAuthResult.Success(null)); // TODO: Implement
|
||||
}
|
||||
89
src/RecNet.Infrastructure/Services/Steam/SteamAuthService.cs
Normal file
89
src/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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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)
|
||||
{
|
||||
SteamPlatformAuth? steamParams;
|
||||
|
||||
try
|
||||
{
|
||||
steamParams = JsonSerializer.Deserialize<SteamPlatformAuth>(platformAuthentication);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return PlatformAuthResult.Failure("Invalid authentication");
|
||||
}
|
||||
|
||||
if (steamParams?.AppId is null || string.IsNullOrWhiteSpace(steamParams.Ticket))
|
||||
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
src/RecNet.Infrastructure/Services/Steam/SteamOptions.cs
Normal file
8
src/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";
|
||||
}
|
||||
9
src/RecNet.Infrastructure/Services/Tokens/JwtOptions.cs
Normal file
9
src/RecNet.Infrastructure/Services/Tokens/JwtOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace RecNet.Infrastructure.Services.Tokens;
|
||||
|
||||
public class JwtOptions
|
||||
{
|
||||
public string Secret { get; set; } = string.Empty;
|
||||
public string Issuer { get; set; } = "http://localhost:5155";
|
||||
public string Audience { get; set; } = "TenWholeYears";
|
||||
public int ExpiryMinutes { get; set; } = 60;
|
||||
}
|
||||
96
src/RecNet.Infrastructure/Services/Tokens/TokenService.cs
Normal file
96
src/RecNet.Infrastructure/Services/Tokens/TokenService.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using RecNet.Application.Common.Interfaces;
|
||||
using RecNet.Application.Common.Tokens;
|
||||
using RecNet.Domain.Profiles;
|
||||
|
||||
namespace RecNet.Infrastructure.Services.Tokens;
|
||||
|
||||
public class TokenService(IOptions<JwtOptions> options) : ITokenService
|
||||
{
|
||||
public TokenResult GenerateProfileToken(Profile profile)
|
||||
{
|
||||
var jwtOptions = options.Value;
|
||||
if (string.IsNullOrWhiteSpace(jwtOptions.Secret))
|
||||
throw new InvalidOperationException("JWT secret is not configured.");
|
||||
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddMinutes(jwtOptions.ExpiryMinutes);
|
||||
var signingCredentials = new SigningCredentials(
|
||||
CreateSecurityKey(jwtOptions),
|
||||
SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, profile.ProfileId.ToString()),
|
||||
new("rn.plat", ((int)profile.Platform).ToString()),
|
||||
new("rn.platid", profile.PlatformId)
|
||||
};
|
||||
|
||||
if (profile.IsModerator)
|
||||
claims.Add(new Claim("role", "moderator"));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: jwtOptions.Issuer,
|
||||
audience: jwtOptions.Audience,
|
||||
claims: claims,
|
||||
expires: expiresAt.UtcDateTime,
|
||||
signingCredentials: signingCredentials);
|
||||
|
||||
return new TokenResult(
|
||||
new JwtSecurityTokenHandler().WriteToken(token),
|
||||
jwtOptions.ExpiryMinutes * 60);
|
||||
}
|
||||
|
||||
public TokenVerifyResult VerifyToken(string token)
|
||||
{
|
||||
var jwtOptions = options.Value;
|
||||
if (string.IsNullOrWhiteSpace(jwtOptions.Secret))
|
||||
throw new InvalidOperationException("JWT secret is not configured.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return TokenVerifyResult.Failure();
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
if (!tokenHandler.CanReadToken(token))
|
||||
return TokenVerifyResult.Failure();
|
||||
|
||||
var validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = CreateSecurityKey(jwtOptions),
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jwtOptions.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtOptions.Audience,
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
|
||||
|
||||
var profileIdClaim =
|
||||
principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ??
|
||||
principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
return Guid.TryParse(profileIdClaim, out var profileId)
|
||||
? TokenVerifyResult.Success(profileId)
|
||||
: TokenVerifyResult.Failure();
|
||||
}
|
||||
catch (SecurityTokenException)
|
||||
{
|
||||
return TokenVerifyResult.Failure();
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return TokenVerifyResult.Failure();
|
||||
}
|
||||
}
|
||||
|
||||
private static SymmetricSecurityKey CreateSecurityKey(JwtOptions jwtOptions)
|
||||
=> new(Encoding.UTF8.GetBytes(jwtOptions.Secret));
|
||||
}
|
||||
16
src/RecNet.MigrationService/Program.cs
Normal file
16
src/RecNet.MigrationService/Program.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using RecNet.MigrationService;
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
using RecNet.ServiceDefaults;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
builder.AddServiceDefaults();
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName));
|
||||
|
||||
builder.AddNpgsqlDbContext<DatabaseContext>("recnet");
|
||||
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
12
src/RecNet.MigrationService/Properties/launchSettings.json
Normal file
12
src/RecNet.MigrationService/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"RecNet.MigrationService": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/RecNet.MigrationService/RecNet.MigrationService.csproj
Normal file
22
src/RecNet.MigrationService/RecNet.MigrationService.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-RecNet.MigrationService-78833cdb-5790-420f-94d2-bec6860bfe4c</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RecNet.Infrastructure\RecNet.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\RecNet.ServiceDefaults\RecNet.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
48
src/RecNet.MigrationService/Worker.cs
Normal file
48
src/RecNet.MigrationService/Worker.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using RecNet.Infrastructure.Persistence;
|
||||
|
||||
namespace RecNet.MigrationService;
|
||||
|
||||
public class Worker(
|
||||
IServiceProvider serviceProvider,
|
||||
IHostApplicationLifetime hostApplicationLifetime) : BackgroundService
|
||||
{
|
||||
public const string ActivitySourceName = "Migrations";
|
||||
private static readonly ActivitySource ActivitySource = new(ActivitySourceName);
|
||||
|
||||
protected override async Task ExecuteAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
"Migrating database", ActivityKind.Client);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||
|
||||
await RunMigrationAsync(dbContext, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.AddException(ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
hostApplicationLifetime.StopApplication();
|
||||
}
|
||||
|
||||
private static async Task RunMigrationAsync(
|
||||
DatabaseContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
// Run migration in a transaction to avoid partial migration if it fails.
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
}
|
||||
8
src/RecNet.MigrationService/appsettings.Development.json
Normal file
8
src/RecNet.MigrationService/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/RecNet.MigrationService/appsettings.json
Normal file
8
src/RecNet.MigrationService/appsettings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/RecNet.ServiceDefaults/Extensions.cs
Normal file
130
src/RecNet.ServiceDefaults/Extensions.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace RecNet.ServiceDefaults;
|
||||
|
||||
// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
|
||||
// This project should be referenced by each service project in your solution.
|
||||
// To learn more about using this project, see https://aka.ms/aspire/service-defaults
|
||||
public static class Extensions
|
||||
{
|
||||
private const string HealthEndpointPath = "/health";
|
||||
private const string AlivenessEndpointPath = "/alive";
|
||||
|
||||
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.ConfigureOpenTelemetry();
|
||||
|
||||
builder.AddDefaultHealthChecks();
|
||||
|
||||
builder.Services.AddServiceDiscovery();
|
||||
|
||||
builder.Services.ConfigureHttpClientDefaults(http =>
|
||||
{
|
||||
// Turn on resilience by default
|
||||
http.AddStandardResilienceHandler();
|
||||
|
||||
// Turn on service discovery by default
|
||||
http.AddServiceDiscovery();
|
||||
});
|
||||
|
||||
// Uncomment the following to restrict the allowed schemes for service discovery.
|
||||
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
|
||||
// {
|
||||
// options.AllowedSchemes = ["https"];
|
||||
// });
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
|
||||
where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation();
|
||||
})
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing.AddSource(builder.Environment.ApplicationName)
|
||||
.AddAspNetCoreInstrumentation(tracing =>
|
||||
// Exclude health check requests from tracing
|
||||
tracing.Filter = context =>
|
||||
!context.Request.Path.StartsWithSegments(HealthEndpointPath)
|
||||
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
|
||||
)
|
||||
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
|
||||
//.AddGrpcClientInstrumentation()
|
||||
.AddHttpClientInstrumentation();
|
||||
});
|
||||
|
||||
builder.AddOpenTelemetryExporters();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder)
|
||||
where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
|
||||
|
||||
if (useOtlpExporter)
|
||||
{
|
||||
builder.Services.AddOpenTelemetry().UseOtlpExporter();
|
||||
}
|
||||
|
||||
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
|
||||
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
|
||||
//{
|
||||
// builder.Services.AddOpenTelemetry()
|
||||
// .UseAzureMonitor();
|
||||
//}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder)
|
||||
where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Services.AddHealthChecks()
|
||||
// Add a default liveness check to ensure app is responsive
|
||||
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
||||
{
|
||||
// Adding health checks endpoints to applications in non-development environments has security implications.
|
||||
// See https://aka.ms/aspire/healthchecks for details before enabling these endpoints in non-development environments.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
// All health checks must pass for app to be considered ready to accept traffic after starting
|
||||
app.MapHealthChecks(HealthEndpointPath);
|
||||
|
||||
// Only health checks tagged with the "live" tag must pass for app to be considered alive
|
||||
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
|
||||
{
|
||||
Predicate = r => r.Tags.Contains("live")
|
||||
});
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
22
src/RecNet.ServiceDefaults/RecNet.ServiceDefaults.csproj
Normal file
22
src/RecNet.ServiceDefaults/RecNet.ServiceDefaults.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireSharedProject>true</IsAspireSharedProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.6.0"/>
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3"/>
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1"/>
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user