From dde68ab8b7fa9f9fdc134b65ef8964cbc3440beb Mon Sep 17 00:00:00 2001 From: Atralupus Date: Thu, 2 Jan 2025 23:53:41 +0900 Subject: [PATCH] Implement ES256KAuthenticationHandler --- .../Auth/ES256KAuthenticationHandlerTests.cs | 228 ++++++++++++++++++ ArenaService.Tests/Auth/JwtCreator.cs | 37 +++ .../AvailableOpponentControllerTests.cs | 37 +-- .../Controllers/BattleControllerTests.cs | 48 ++-- .../Controllers/ControllerTestUtils.cs | 21 ++ .../Controllers/LeaderboardControllerTests.cs | 10 +- .../Controllers/ParticipantControllerTests.cs | 67 +++-- .../Controllers/SeasonControllerTests.cs | 22 +- ArenaService/ArenaService.csproj | 1 + .../Auth/ES256KAuthenticationHandler.cs | 181 ++++++++++++++ .../AvailableOpponentController.cs | 47 ++-- ArenaService/Controllers/BattleController.cs | 46 ++-- .../Controllers/LeaderboardController.cs | 8 +- .../Controllers/ParticipantController.cs | 34 ++- ArenaService/Controllers/SeasonController.cs | 24 +- ArenaService/Dtos/JoinRequest.cs | 2 - .../Extensions/ClaimsPrincipalExtensions.cs | 14 ++ ArenaService/Program.cs | 45 +--- .../Services/AvailableOpponentService.cs | 25 -- ArenaService/Services/ParticipaintService.cs | 42 ---- ArenaService/Services/SeasonService.cs | 37 --- ArenaService/Setup.cs | 107 ++++++++ 22 files changed, 739 insertions(+), 344 deletions(-) create mode 100644 ArenaService.Tests/Auth/ES256KAuthenticationHandlerTests.cs create mode 100644 ArenaService.Tests/Auth/JwtCreator.cs create mode 100644 ArenaService.Tests/Controllers/ControllerTestUtils.cs create mode 100644 ArenaService/Auth/ES256KAuthenticationHandler.cs create mode 100644 ArenaService/Extensions/ClaimsPrincipalExtensions.cs delete mode 100644 ArenaService/Services/AvailableOpponentService.cs delete mode 100644 ArenaService/Services/ParticipaintService.cs delete mode 100644 ArenaService/Services/SeasonService.cs create mode 100644 ArenaService/Setup.cs diff --git a/ArenaService.Tests/Auth/ES256KAuthenticationHandlerTests.cs b/ArenaService.Tests/Auth/ES256KAuthenticationHandlerTests.cs new file mode 100644 index 0000000..1c6a61b --- /dev/null +++ b/ArenaService.Tests/Auth/ES256KAuthenticationHandlerTests.cs @@ -0,0 +1,228 @@ +namespace ArenaService.Tests.Auth; + +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using global::ArenaService.Auth; +using Libplanet.Common; +using Libplanet.Crypto; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Newtonsoft.Json; +using Xunit; + +public class ES256KAuthenticationHandlerTests +{ + private readonly Mock> _optionsMonitor; + private readonly Mock _loggerFactory; + private readonly Mock _encoder; + private readonly Mock _clock; + + public ES256KAuthenticationHandlerTests() + { + _optionsMonitor = new Mock>(); + _optionsMonitor + .Setup(m => m.Get(It.IsAny())) + .Returns(new AuthenticationSchemeOptions()); + _optionsMonitor.Setup(m => m.CurrentValue).Returns(new AuthenticationSchemeOptions()); + _loggerFactory = new Mock(); + var mockLogger = new Mock>(); + _loggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(mockLogger.Object); + _encoder = new Mock(); + _clock = new Mock(); + } + + [Fact] + public async Task HandleAuthenticateAsync_ValidUserRole_ReturnsSuccess() + { + var privateKey = new PrivateKey(); + var jwt = JwtCreator.CreateJwt(privateKey, role: "User"); + var publicKey = privateKey.PublicKey; + var address = publicKey.Address.ToString(); + + var context = new DefaultHttpContext(); + context.Request.Headers["Authorization"] = $"Bearer {jwt}"; + + var handler = new ES256KAuthenticationHandler( + _optionsMonitor.Object, + _loggerFactory.Object, + _encoder.Object, + _clock.Object + ); + + await handler.InitializeAsync( + new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)), + context + ); + + var result = await handler.AuthenticateAsync(); + + Assert.True(result.Succeeded); + Assert.NotNull(result.Principal); + Assert.Equal(publicKey.ToString(), result.Principal.FindFirst("public_key")?.Value); + Assert.Equal(address, result.Principal.FindFirst("address")?.Value); + Assert.Equal("test", result.Principal.FindFirst("avatar_address")?.Value); + } + + [Fact] + public async Task HandleAuthenticateAsync_ValidAdminRole_ReturnsSuccess() + { + var privateKey = new PrivateKey(); + var jwt = JwtCreator.CreateJwt(privateKey, role: "Admin"); + var publicKey = privateKey.PublicKey; + + Environment.SetEnvironmentVariable("ALLOWED_ADMIN_PUBLIC_KEY", publicKey.ToHex(true)); + + var context = new DefaultHttpContext(); + context.Request.Headers["Authorization"] = $"Bearer {jwt}"; + + var handler = new ES256KAuthenticationHandler( + _optionsMonitor.Object, + _loggerFactory.Object, + _encoder.Object, + _clock.Object + ); + + await handler.InitializeAsync( + new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)), + context + ); + + var result = await handler.AuthenticateAsync(); + + Assert.True(result.Succeeded); + Assert.NotNull(result.Principal); + Assert.Equal(publicKey.ToString(), result.Principal.FindFirst("public_key")?.Value); + } + + [Fact] + public async Task HandleAuthenticateAsync_InvalidAdminKey_ReturnsFail() + { + var privateKey = new PrivateKey(); + var jwt = JwtCreator.CreateJwt(privateKey, role: "Admin"); + + Environment.SetEnvironmentVariable( + "ALLOWED_ADMIN_PUBLIC_KEY", + new PrivateKey().PublicKey.ToHex(true) + ); + + var context = new DefaultHttpContext(); + context.Request.Headers["Authorization"] = $"Bearer {jwt}"; + + var handler = new ES256KAuthenticationHandler( + _optionsMonitor.Object, + _loggerFactory.Object, + _encoder.Object, + _clock.Object + ); + + await handler.InitializeAsync( + new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)), + context + ); + + var result = await handler.AuthenticateAsync(); + + Assert.False(result.Succeeded); + Assert.Null(result.Principal); + } + + [Fact] + public async Task HandleAuthenticateAsync_MissingToken_ReturnsFail() + { + var context = new DefaultHttpContext(); + var handler = new ES256KAuthenticationHandler( + _optionsMonitor.Object, + _loggerFactory.Object, + _encoder.Object, + _clock.Object + ); + + await handler.InitializeAsync( + new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)), + context + ); + + var result = await handler.AuthenticateAsync(); + + Assert.False(result.Succeeded); + Assert.Null(result.Principal); + Assert.Contains("Missing or invalid Authorization header.", result.Failure.Message); + } + + [Fact] + public async Task HandleAuthenticateAsync_InvalidTokenFormat_ReturnsFail() + { + var context = new DefaultHttpContext(); + context.Request.Headers["Authorization"] = "Bearer invalid.token.format"; + + var handler = new ES256KAuthenticationHandler( + _optionsMonitor.Object, + _loggerFactory.Object, + _encoder.Object, + _clock.Object + ); + + await handler.InitializeAsync( + new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)), + context + ); + + var result = await handler.AuthenticateAsync(); + + Assert.False(result.Succeeded); + Assert.Null(result.Principal); + Assert.Contains("Invalid token.", result.Failure.Message); + } + + [Fact] + public async Task HandleAuthenticateAsync_ExpiredToken_ReturnsFail() + { + var privateKey = new PrivateKey(); + var payload = new + { + iss = "user", + avt_adr = "test", + pbk = privateKey.PublicKey.ToHex(true), + role = "User", + iat = DateTimeOffset.UtcNow.AddHours(-2).ToUnixTimeSeconds(), + exp = DateTimeOffset.UtcNow.AddHours(-1).ToUnixTimeSeconds() + }; + + string payloadJson = JsonConvert.SerializeObject(payload); + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadJson); + byte[] hash = HashDigest.DeriveFrom(payloadBytes).ToByteArray(); + byte[] signature = privateKey.Sign(hash); + + string signatureBase64 = Convert.ToBase64String(signature); + string headerJson = JsonConvert.SerializeObject(new { alg = "ES256K", typ = "JWT" }); + string headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(headerJson)); + string payloadBase64 = Convert.ToBase64String(payloadBytes); + + var jwt = $"{headerBase64}.{payloadBase64}.{signatureBase64}"; + + var context = new DefaultHttpContext(); + context.Request.Headers["Authorization"] = $"Bearer {jwt}"; + + var handler = new ES256KAuthenticationHandler( + _optionsMonitor.Object, + _loggerFactory.Object, + _encoder.Object, + _clock.Object + ); + + await handler.InitializeAsync( + new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)), + context + ); + + var result = await handler.AuthenticateAsync(); + + Assert.False(result.Succeeded); + Assert.Null(result.Principal); + Assert.Contains("Invalid token.", result.Failure.Message); + } +} diff --git a/ArenaService.Tests/Auth/JwtCreator.cs b/ArenaService.Tests/Auth/JwtCreator.cs new file mode 100644 index 0000000..481ec96 --- /dev/null +++ b/ArenaService.Tests/Auth/JwtCreator.cs @@ -0,0 +1,37 @@ +namespace ArenaService.Tests.Auth; + +using System; +using System.Security.Cryptography; +using System.Text; +using Libplanet.Common; +using Libplanet.Crypto; +using Newtonsoft.Json; + +public class JwtCreator +{ + public static string CreateJwt(PrivateKey privateKey, string role = "user") + { + var payload = new + { + iss = "user", + avt_adr = "test", + sub = privateKey.PublicKey.ToHex(true), + role, + iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + exp = DateTimeOffset.UtcNow.AddMinutes(60).ToUnixTimeSeconds() + }; + + string payloadJson = JsonConvert.SerializeObject(payload); + byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadJson); + + byte[] signature = privateKey.Sign(payloadBytes); + + string signatureBase64 = Convert.ToBase64String(signature); + + string headerJson = JsonConvert.SerializeObject(new { alg = "ES256K", typ = "JWT" }); + string headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(headerJson)); + string payloadBase64 = Convert.ToBase64String(payloadBytes); + + return $"{headerBase64}.{payloadBase64}.{signatureBase64}"; + } +} diff --git a/ArenaService.Tests/Controllers/AvailableOpponentControllerTests.cs b/ArenaService.Tests/Controllers/AvailableOpponentControllerTests.cs index c12afcc..84fa622 100644 --- a/ArenaService.Tests/Controllers/AvailableOpponentControllerTests.cs +++ b/ArenaService.Tests/Controllers/AvailableOpponentControllerTests.cs @@ -3,7 +3,7 @@ using ArenaService.Dtos; using ArenaService.Models; using ArenaService.Repositories; -using ArenaService.Services; +using ArenaService.Tests.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -14,24 +14,18 @@ namespace ArenaService.Tests.Controllers; public class AvailableOpponentControllerTests { private readonly AvailableOpponentController _controller; - private Mock _availableOpponentRepositoryMock; - private AvailableOpponentService _availableOpponentService; - private Mock _participantRepositoryMock; - private ParticipantService _participantService; + private Mock _availableOpponentRepoMock; + private Mock _participantRepoMock; public AvailableOpponentControllerTests() { - var availableOpponentRepositoryMock = new Mock(); - _availableOpponentRepositoryMock = availableOpponentRepositoryMock; - _availableOpponentService = new AvailableOpponentService( - _availableOpponentRepositoryMock.Object - ); - var participantRepositoryMock = new Mock(); - _participantRepositoryMock = participantRepositoryMock; - _participantService = new ParticipantService(_participantRepositoryMock.Object); + var availableOpponentRepoMock = new Mock(); + _availableOpponentRepoMock = availableOpponentRepoMock; + var participantRepoMock = new Mock(); + _participantRepoMock = participantRepoMock; _controller = new AvailableOpponentController( - _availableOpponentService, - _participantService + _availableOpponentRepoMock.Object, + _participantRepoMock.Object ); } @@ -39,8 +33,9 @@ public AvailableOpponentControllerTests() public async Task GetAvailableOpponents_WithValidHeader_ReturnsOk() { var avatarAddress = "DDF1472fD5a79B8F46C28e7643eDEF045e36BD3d"; + ControllerTestUtils.ConfigureMockHttpContextWithAuth(_controller, avatarAddress); - _participantRepositoryMock + _participantRepoMock .Setup(repo => repo.GetParticipantByAvatarAddressAsync(1, avatarAddress)) .ReturnsAsync( new Participant @@ -52,7 +47,7 @@ public async Task GetAvailableOpponents_WithValidHeader_ReturnsOk() } ); - _availableOpponentRepositoryMock + _availableOpponentRepoMock .Setup(repo => repo.GetAvailableOpponents(1)) .ReturnsAsync( [ @@ -87,14 +82,6 @@ public async Task GetAvailableOpponents_WithValidHeader_ReturnsOk() ] ); - _controller.ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; - _controller.ControllerContext.HttpContext.User = new ClaimsPrincipal( - new ClaimsIdentity([new Claim("avatar", avatarAddress)]) - ); - var result = await _controller.GetAvailableOpponents(1); var okResult = Assert.IsType>(result.Result); diff --git a/ArenaService.Tests/Controllers/BattleControllerTests.cs b/ArenaService.Tests/Controllers/BattleControllerTests.cs index 7fc1068..c5fa141 100644 --- a/ArenaService.Tests/Controllers/BattleControllerTests.cs +++ b/ArenaService.Tests/Controllers/BattleControllerTests.cs @@ -1,9 +1,8 @@ using System.Security.Claims; using ArenaService.Controllers; -using ArenaService.Dtos; using ArenaService.Models; using ArenaService.Repositories; -using ArenaService.Services; +using ArenaService.Tests.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -14,28 +13,22 @@ namespace ArenaService.Tests.Controllers; public class BattleControllerTests { private readonly BattleController _controller; - private Mock _availableOpponentRepositoryMock; - private AvailableOpponentService _availableOpponentService; - private Mock _participantRepositoryMock; - private Mock _battleLogRepositoryMock; - private ParticipantService _participantService; + private Mock _availableOpponentRepoMock; + private Mock _participantRepoMock; + private Mock _battleLogRepoMock; public BattleControllerTests() { - var availableOpponentRepositoryMock = new Mock(); - _availableOpponentRepositoryMock = availableOpponentRepositoryMock; - _availableOpponentService = new AvailableOpponentService( - _availableOpponentRepositoryMock.Object - ); - var participantRepositoryMock = new Mock(); - _participantRepositoryMock = participantRepositoryMock; - _participantService = new ParticipantService(_participantRepositoryMock.Object); - var battleLogRepositoryMock = new Mock(); - _battleLogRepositoryMock = battleLogRepositoryMock; + var availableOpponentRepoMock = new Mock(); + _availableOpponentRepoMock = availableOpponentRepoMock; + var participantRepoMock = new Mock(); + _participantRepoMock = participantRepoMock; + var battleLogRepoMock = new Mock(); + _battleLogRepoMock = battleLogRepoMock; _controller = new BattleController( - _availableOpponentService, - _participantService, - _battleLogRepositoryMock.Object + _availableOpponentRepoMock.Object, + _participantRepoMock.Object, + _battleLogRepoMock.Object ); } @@ -43,8 +36,9 @@ public BattleControllerTests() public async Task GetBattleToken_WithValidHeader_ReturnsOk() { var avatarAddress = "DDF1472fD5a79B8F46C28e7643eDEF045e36BD3d"; + ControllerTestUtils.ConfigureMockHttpContextWithAuth(_controller, avatarAddress); - _participantRepositoryMock + _participantRepoMock .Setup(repo => repo.GetParticipantByAvatarAddressAsync(1, avatarAddress)) .ReturnsAsync( new Participant @@ -56,7 +50,7 @@ public async Task GetBattleToken_WithValidHeader_ReturnsOk() } ); - _availableOpponentRepositoryMock + _availableOpponentRepoMock .Setup(repo => repo.GetAvailableOpponents(1)) .ReturnsAsync( [ @@ -91,7 +85,7 @@ public async Task GetBattleToken_WithValidHeader_ReturnsOk() ] ); - _battleLogRepositoryMock + _battleLogRepoMock .Setup(repo => repo.AddBattleLogAsync(1, 1, 1, "token")) .ReturnsAsync( new BattleLog @@ -104,14 +98,6 @@ public async Task GetBattleToken_WithValidHeader_ReturnsOk() } ); - _controller.ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; - _controller.ControllerContext.HttpContext.User = new ClaimsPrincipal( - new ClaimsIdentity([new Claim("avatar", avatarAddress)]) - ); - var result = await _controller.CreateBattleToken(1, 1); var okResult = Assert.IsType>(result.Result); diff --git a/ArenaService.Tests/Controllers/ControllerTestUtils.cs b/ArenaService.Tests/Controllers/ControllerTestUtils.cs new file mode 100644 index 0000000..2108752 --- /dev/null +++ b/ArenaService.Tests/Controllers/ControllerTestUtils.cs @@ -0,0 +1,21 @@ +namespace ArenaService.Tests.Utils; + +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +public static class ControllerTestUtils +{ + public static void ConfigureMockHttpContextWithAuth( + ControllerBase controller, + string avatarAddress + ) + { + var user = new ClaimsPrincipal( + new ClaimsIdentity([new Claim("avatar_address", avatarAddress)], "mock") + ); + + var httpContext = new DefaultHttpContext { User = user }; + controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; + } +} diff --git a/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs b/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs index 52950f0..4b7c167 100644 --- a/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs +++ b/ArenaService.Tests/Controllers/LeaderboardControllerTests.cs @@ -10,19 +10,19 @@ namespace ArenaService.Tests.Controllers; public class LeaderboardControllerTests { private readonly LeaderboardController _controller; - private Mock _leaderboardRepositoryMock; + private Mock _leaderboardRepoMock; public LeaderboardControllerTests() { - var leaderboardRepositoryMock = new Mock(); - _leaderboardRepositoryMock = leaderboardRepositoryMock; - _controller = new LeaderboardController(_leaderboardRepositoryMock.Object); + var leaderboardRepoMock = new Mock(); + _leaderboardRepoMock = leaderboardRepoMock; + _controller = new LeaderboardController(_leaderboardRepoMock.Object); } [Fact] public async Task Join_ActivatedSeasonsExist_ReturnsOk() { - _leaderboardRepositoryMock + _leaderboardRepoMock .Setup(repo => repo.GetMyRankAsync(1, 1)) .ReturnsAsync( new LeaderboardEntry diff --git a/ArenaService.Tests/Controllers/ParticipantControllerTests.cs b/ArenaService.Tests/Controllers/ParticipantControllerTests.cs index 6a44d47..bb6dd04 100644 --- a/ArenaService.Tests/Controllers/ParticipantControllerTests.cs +++ b/ArenaService.Tests/Controllers/ParticipantControllerTests.cs @@ -1,8 +1,10 @@ -using ArenaService.Controllers; +using System.Security.Claims; +using ArenaService.Controllers; using ArenaService.Dtos; using ArenaService.Models; using ArenaService.Repositories; -using ArenaService.Services; +using ArenaService.Tests.Utils; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Moq; @@ -12,25 +14,26 @@ namespace ArenaService.Tests.Controllers; public class ParticipantControllerTests { private readonly ParticipantController _controller; - private Mock _seasonRepositoryMock; - private Mock _participantRepositoryMock; - private SeasonService _seasonService; - private ParticipantService _participantService; + private Mock _seasonRepoMock; + private Mock _participantRepoMock; public ParticipantControllerTests() { - var seasonRepositoryMock = new Mock(); - var participantRepositoryMock = new Mock(); - _seasonRepositoryMock = seasonRepositoryMock; - _participantRepositoryMock = participantRepositoryMock; - _seasonService = new SeasonService(_seasonRepositoryMock.Object); - _participantService = new ParticipantService(_participantRepositoryMock.Object); - _controller = new ParticipantController(_participantService, _seasonService); + var seasonRepoMock = new Mock(); + var participantRepoMock = new Mock(); + _seasonRepoMock = seasonRepoMock; + _participantRepoMock = participantRepoMock; + _controller = new ParticipantController( + _participantRepoMock.Object, + _seasonRepoMock.Object + ); } [Fact] public async Task Join_ActivatedSeasonsExist_ReturnsOk() { + ControllerTestUtils.ConfigureMockHttpContextWithAuth(_controller, "test-avatar-address"); + var season = new Season { Id = 1, @@ -49,8 +52,8 @@ public async Task Join_ActivatedSeasonsExist_ReturnsOk() Season = season }; - _seasonRepositoryMock.Setup(repo => repo.GetSeasonAsync(season.Id)).ReturnsAsync(season); - _participantRepositoryMock + _seasonRepoMock.Setup(repo => repo.GetSeasonAsync(season.Id)).ReturnsAsync(season); + _participantRepoMock .Setup(repo => repo.InsertParticipantToSpecificSeasonAsync( season.Id, @@ -63,13 +66,7 @@ public async Task Join_ActivatedSeasonsExist_ReturnsOk() var result = await _controller.Join( 1, - new JoinRequest - { - AvatarAddress = "test", - AuthToken = "test", - NameWithHash = "test", - PortraitId = 1 - } + new JoinRequest { NameWithHash = "test", PortraitId = 1 } ); var okResult = Assert.IsType(result.Result); @@ -78,6 +75,7 @@ public async Task Join_ActivatedSeasonsExist_ReturnsOk() [Fact] public async Task Join_ActivatedSeasonsNotExist_ReturnsNotFound() { + ControllerTestUtils.ConfigureMockHttpContextWithAuth(_controller, "test-avatar-address"); var season = new Season { Id = 1, @@ -96,8 +94,8 @@ public async Task Join_ActivatedSeasonsNotExist_ReturnsNotFound() Season = season }; - _seasonRepositoryMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync([season]); - _participantRepositoryMock + _seasonRepoMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync([season]); + _participantRepoMock .Setup(repo => repo.InsertParticipantToSpecificSeasonAsync( season.Id, @@ -110,13 +108,7 @@ public async Task Join_ActivatedSeasonsNotExist_ReturnsNotFound() var result = await _controller.Join( 1, - new JoinRequest - { - AvatarAddress = "test", - AuthToken = "test", - NameWithHash = "test", - PortraitId = 1 - } + new JoinRequest { NameWithHash = "test", PortraitId = 1 } ); Assert.IsType>(result.Result); @@ -125,6 +117,7 @@ public async Task Join_ActivatedSeasonsNotExist_ReturnsNotFound() [Fact] public async Task Join_SeasonsNotExist_ReturnsNotFound() { + ControllerTestUtils.ConfigureMockHttpContextWithAuth(_controller, "test-avatar-address"); var season = new Season { Id = 1, @@ -143,8 +136,8 @@ public async Task Join_SeasonsNotExist_ReturnsNotFound() Season = season }; - _seasonRepositoryMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync([season]); - _participantRepositoryMock + _seasonRepoMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync([season]); + _participantRepoMock .Setup(repo => repo.InsertParticipantToSpecificSeasonAsync( season.Id, @@ -157,13 +150,7 @@ public async Task Join_SeasonsNotExist_ReturnsNotFound() var result = await _controller.Join( 2, - new JoinRequest - { - AvatarAddress = "test", - AuthToken = "test", - NameWithHash = "test", - PortraitId = 1 - } + new JoinRequest { NameWithHash = "test", PortraitId = 1 } ); var okResult = Assert.IsType>(result.Result); diff --git a/ArenaService.Tests/Controllers/SeasonControllerTests.cs b/ArenaService.Tests/Controllers/SeasonControllerTests.cs index 6a32d57..babb958 100644 --- a/ArenaService.Tests/Controllers/SeasonControllerTests.cs +++ b/ArenaService.Tests/Controllers/SeasonControllerTests.cs @@ -2,9 +2,7 @@ using ArenaService.Dtos; using ArenaService.Models; using ArenaService.Repositories; -using ArenaService.Services; using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; using Moq; namespace ArenaService.Tests.Controllers; @@ -12,15 +10,13 @@ namespace ArenaService.Tests.Controllers; public class SeasonControllerTests { private readonly SeasonController _controller; - private Mock _repositoryMock; - private SeasonService _service; + private Mock _repoMock; public SeasonControllerTests() { - var repositoryMock = new Mock(); - _repositoryMock = repositoryMock; - _service = new SeasonService(repositoryMock.Object); - _controller = new SeasonController(_service); + var repoMock = new Mock(); + _repoMock = repoMock; + _controller = new SeasonController(_repoMock.Object); } [Fact] @@ -47,7 +43,7 @@ public async Task GetCurrentSeason_ActivatedSeasonsExist_ReturnsOkWithCorrectSea } }; - _repositoryMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync(seasons); + _repoMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync(seasons); var result = await _controller.GetCurrentSeason(blockIndex); @@ -87,7 +83,7 @@ public async Task GetCurrentSeason_MultipleSeasonsSameBlockIndex_ReturnsFirstMat } }; - _repositoryMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync(seasons); + _repoMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync(seasons); var result = await _controller.GetCurrentSeason(blockIndex); @@ -128,7 +124,7 @@ public async Task GetCurrentSeason_NoMatchingSeasonExists_ReturnsNotFound() } }; - _repositoryMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync(seasons); + _repoMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync(seasons); var result = await _controller.GetCurrentSeason(blockIndex); @@ -140,9 +136,7 @@ public async Task GetCurrentSeason_NoActivatedSeasons_ReturnsNotFound() { var blockIndex = 1000; - _repositoryMock - .Setup(repo => repo.GetActivatedSeasonsAsync()) - .ReturnsAsync(new List()); + _repoMock.Setup(repo => repo.GetActivatedSeasonsAsync()).ReturnsAsync(new List()); var result = await _controller.GetCurrentSeason(blockIndex); diff --git a/ArenaService/ArenaService.csproj b/ArenaService/ArenaService.csproj index fbaae8f..0daac40 100644 --- a/ArenaService/ArenaService.csproj +++ b/ArenaService/ArenaService.csproj @@ -8,6 +8,7 @@ + diff --git a/ArenaService/Auth/ES256KAuthenticationHandler.cs b/ArenaService/Auth/ES256KAuthenticationHandler.cs new file mode 100644 index 0000000..071b485 --- /dev/null +++ b/ArenaService/Auth/ES256KAuthenticationHandler.cs @@ -0,0 +1,181 @@ +namespace ArenaService.Auth; + +using System.Collections.Immutable; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using Libplanet.Crypto; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +public class ES256KAuthenticationHandler : AuthenticationHandler +{ + public ES256KAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock + ) + : base(options, logger, encoder, clock) { } + + protected override Task HandleAuthenticateAsync() + { + var authorizationHeader = GetAuthorizationHeader(); + if (string.IsNullOrEmpty(authorizationHeader)) + { + return Task.FromResult( + AuthenticateResult.Fail("Missing or invalid Authorization header.") + ); + } + + var token = GetTokenFromHeader(authorizationHeader); + if ( + !TryExtractTokenParts( + token, + out var payloadBytes, + out var signature, + out var publicKey, + out var address, + out var avtAdr, + out var role + ) + ) + { + return Task.FromResult(AuthenticateResult.Fail("Invalid token.")); + } + + if (!ValidateSignature(payloadBytes, signature, publicKey)) + { + return Task.FromResult(AuthenticateResult.Fail("Invalid signature.")); + } + + if (role == "Admin" && !ValidateAdminKey(publicKey)) + { + return Task.FromResult(AuthenticateResult.Fail("Unauthorized Admin key.")); + } + + return Task.FromResult( + AuthenticateResult.Success(CreateAuthenticationTicket(publicKey, address, avtAdr, role)) + ); + } + + private string GetAuthorizationHeader() + { + return Request.Headers["Authorization"].ToString(); + } + + private string GetTokenFromHeader(string authorizationHeader) + { + return authorizationHeader.StartsWith("Bearer ") + ? authorizationHeader.Substring("Bearer ".Length).Trim() + : string.Empty; + } + + private bool TryExtractTokenParts( + string jwt, + out byte[] payloadBytes, + out byte[] signature, + out string publicKey, + out string address, + out string avtAdr, + out string role + ) + { + payloadBytes = null; + signature = null; + publicKey = null; + address = null; + avtAdr = null; + role = null; + + var parts = jwt.Split('.'); + if (parts.Length != 3) + { + return false; + } + + try + { + payloadBytes = Convert.FromBase64String(parts[1]); + signature = Convert.FromBase64String(parts[2]); + + var payload = Encoding.UTF8.GetString(payloadBytes); + var payloadJson = JsonSerializer.Deserialize(payload); + + publicKey = payloadJson.GetProperty("sub").GetString(); + avtAdr = payloadJson.GetProperty("avt_adr").GetString(); + role = payloadJson.GetProperty("role").GetString(); + + if ( + string.IsNullOrEmpty(publicKey) + || string.IsNullOrEmpty(avtAdr) + || string.IsNullOrEmpty(role) + ) + { + return false; + } + + var pubKey = PublicKey.FromHex(publicKey); + address = pubKey.Address.ToString(); + + return true; + } + catch + { + return false; + } + } + + private bool ValidateSignature(byte[] payloadBytes, byte[] signature, string publicKey) + { + try + { + var pubKey = PublicKey.FromHex(publicKey); + + return pubKey.Verify(payloadBytes, signature); + } + catch + { + return false; + } + } + + private bool ValidateAdminKey(string publicKey) + { + var allowedAdminKey = Environment.GetEnvironmentVariable("ALLOWED_ADMIN_PUBLIC_KEY"); + if (string.IsNullOrEmpty(allowedAdminKey)) + { + return false; + } + + try + { + var adminKey = PublicKey.FromHex(allowedAdminKey); + return adminKey.ToHex(true) == publicKey; + } + catch + { + return false; + } + } + + private AuthenticationTicket CreateAuthenticationTicket( + string publicKey, + string address, + string avtAdr, + string role + ) + { + var claims = new[] + { + new Claim(ClaimTypes.Role, role), + new Claim("public_key", publicKey), + new Claim("address", address), + new Claim("avatar_address", avtAdr) + }; + var identity = new ClaimsIdentity(claims, Scheme.Name); + var principal = new ClaimsPrincipal(identity); + return new AuthenticationTicket(principal, Scheme.Name); + } +} diff --git a/ArenaService/Controllers/AvailableOpponentController.cs b/ArenaService/Controllers/AvailableOpponentController.cs index 1fdaaba..6ef07c0 100644 --- a/ArenaService/Controllers/AvailableOpponentController.cs +++ b/ArenaService/Controllers/AvailableOpponentController.cs @@ -1,9 +1,9 @@ namespace ArenaService.Controllers; -using System.Security.Claims; using ArenaService.Dtos; using ArenaService.Extensions; -using ArenaService.Services; +using ArenaService.Repositories; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -11,41 +11,27 @@ namespace ArenaService.Controllers; [ApiController] public class AvailableOpponentController : ControllerBase { - private readonly AvailableOpponentService _availableOpponentService; - private readonly ParticipantService _participantService; + private readonly IAvailableOpponentRepository _availableOpponentRepo; + private readonly IParticipantRepository _participantRepo; public AvailableOpponentController( - AvailableOpponentService availableOpponentService, - ParticipantService participantService + IAvailableOpponentRepository availableOpponentRepo, + IParticipantRepository participantRepo ) { - _availableOpponentService = availableOpponentService; - _participantService = participantService; - } - - private string? ExtractAvatarAddress() - { - if (HttpContext.User.Identity is ClaimsIdentity identity) - { - var claim = identity.FindFirst("avatar"); - return claim?.Value; - } - return null; + _availableOpponentRepo = availableOpponentRepo; + _participantRepo = participantRepo; } [HttpGet] + [Authorize(Roles = "User", AuthenticationSchemes = "ES256K")] public async Task< Results, Ok> > GetAvailableOpponents(int seasonId) { - var avatarAddress = ExtractAvatarAddress(); + var avatarAddress = HttpContext.User.RequireAvatarAddress(); - if (avatarAddress is null) - { - return TypedResults.Unauthorized(); - } - - var participant = await _participantService.GetParticipantByAvatarAddressAsync( + var participant = await _participantRepo.GetParticipantByAvatarAddressAsync( seasonId, avatarAddress ); @@ -55,7 +41,8 @@ public async Task< return TypedResults.NotFound("Not participant user."); } - var opponents = await _availableOpponentService.GetAvailableOpponents(participant.Id); + var availableOpponents = await _availableOpponentRepo.GetAvailableOpponents(participant.Id); + var opponents = availableOpponents.Select(ao => ao.Opponent).ToList(); return TypedResults.Ok( new AvailableOpponentsResponse { AvailableOpponents = opponents.ToResponse() } @@ -63,16 +50,12 @@ public async Task< } [HttpPost] + [Authorize(Roles = "User", AuthenticationSchemes = "ES256K")] public async Task, Created>> ResetOpponents( int seasonId ) { - var avatarAddress = ExtractAvatarAddress(); - - if (avatarAddress is null) - { - return TypedResults.Unauthorized(); - } + var avatarAddress = HttpContext.User.RequireAvatarAddress(); // Dummy implementation return TypedResults.Created(); diff --git a/ArenaService/Controllers/BattleController.cs b/ArenaService/Controllers/BattleController.cs index 99dd7be..5a6aa7c 100644 --- a/ArenaService/Controllers/BattleController.cs +++ b/ArenaService/Controllers/BattleController.cs @@ -1,10 +1,8 @@ namespace ArenaService.Controllers; -using System.Security.Claims; -using ArenaService.Dtos; using ArenaService.Extensions; using ArenaService.Repositories; -using ArenaService.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -12,44 +10,30 @@ namespace ArenaService.Controllers; [ApiController] public class BattleController : ControllerBase { - private readonly AvailableOpponentService _availableOpponentService; - private readonly ParticipantService _participantService; - private readonly IBattleLogRepository _battleLogRepository; + private readonly IAvailableOpponentRepository _availableOpponentRepo; + private readonly IParticipantRepository _participantRepo; + private readonly IBattleLogRepository _battleLogRepo; public BattleController( - AvailableOpponentService availableOpponentService, - ParticipantService participantService, - IBattleLogRepository battleLogRepository + IAvailableOpponentRepository availableOpponentService, + IParticipantRepository participantService, + IBattleLogRepository battleLogRepo ) { - _availableOpponentService = availableOpponentService; - _participantService = participantService; - _battleLogRepository = battleLogRepository; - } - - private string? ExtractAvatarAddress() - { - if (HttpContext.User.Identity is ClaimsIdentity identity) - { - var claim = identity.FindFirst("avatar"); - return claim?.Value; - } - return null; + _availableOpponentRepo = availableOpponentService; + _participantRepo = participantService; + _battleLogRepo = battleLogRepo; } [HttpPost] + [Authorize(Roles = "User", AuthenticationSchemes = "ES256K")] public async Task< Results, Ok> > CreateBattleToken(int seasonId, int opponentId) { - var avatarAddress = ExtractAvatarAddress(); - - if (avatarAddress is null) - { - return TypedResults.Unauthorized(); - } + var avatarAddress = HttpContext.User.RequireAvatarAddress(); - var participant = await _participantService.GetParticipantByAvatarAddressAsync( + var participant = await _participantRepo.GetParticipantByAvatarAddressAsync( seasonId, avatarAddress ); @@ -59,8 +43,8 @@ public async Task< return TypedResults.NotFound("Not participant user."); } - var opponents = await _availableOpponentService.GetAvailableOpponents(participant.Id); - var battleLog = await _battleLogRepository.AddBattleLogAsync( + var opponents = await _availableOpponentRepo.GetAvailableOpponents(participant.Id); + var battleLog = await _battleLogRepo.AddBattleLogAsync( participant.Id, opponentId, seasonId, diff --git a/ArenaService/Controllers/LeaderboardController.cs b/ArenaService/Controllers/LeaderboardController.cs index a38afb7..95ca2ff 100644 --- a/ArenaService/Controllers/LeaderboardController.cs +++ b/ArenaService/Controllers/LeaderboardController.cs @@ -10,11 +10,11 @@ namespace ArenaService.Controllers; [ApiController] public class LeaderboardController : ControllerBase { - private readonly ILeaderboardRepository _leaderboardRepository; + private readonly ILeaderboardRepository _leaderboardRepo; - public LeaderboardController(ILeaderboardRepository leaderboardRepository) + public LeaderboardController(ILeaderboardRepository leaderboardRepo) { - _leaderboardRepository = leaderboardRepository; + _leaderboardRepo = leaderboardRepo; } // [HttpGet] @@ -33,7 +33,7 @@ public async Task, Ok>> GetMy int participantId ) { - var leaderboardEntry = await _leaderboardRepository.GetMyRankAsync(seasonId, participantId); + var leaderboardEntry = await _leaderboardRepo.GetMyRankAsync(seasonId, participantId); if (leaderboardEntry == null) { diff --git a/ArenaService/Controllers/ParticipantController.cs b/ArenaService/Controllers/ParticipantController.cs index 40e2066..64c5e50 100644 --- a/ArenaService/Controllers/ParticipantController.cs +++ b/ArenaService/Controllers/ParticipantController.cs @@ -1,8 +1,9 @@ namespace ArenaService.Controllers; using ArenaService.Dtos; -using ArenaService.Exceptions; -using ArenaService.Services; +using ArenaService.Extensions; +using ArenaService.Repositories; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -10,24 +11,37 @@ namespace ArenaService.Controllers; [ApiController] public class ParticipantController : ControllerBase { - private readonly ParticipantService _participantService; - private readonly SeasonService _seasonService; + private readonly IParticipantRepository _participantRepo; + private readonly ISeasonRepository _seasonRepo; - public ParticipantController(ParticipantService participantService, SeasonService seasonService) + public ParticipantController( + IParticipantRepository participantRepo, + ISeasonRepository seasonRepo + ) { - _participantService = participantService; - _seasonService = seasonService; + _participantRepo = participantRepo; + _seasonRepo = seasonRepo; } [HttpPost] - public async Task, Created>> Join( + [Authorize(Roles = "User", AuthenticationSchemes = "ES256K")] + public async Task, Created>> Join( int seasonId, [FromBody] JoinRequest joinRequest ) { - if (await _seasonService.IsActivatedSeason(seasonId)) + var avatarAddress = HttpContext.User.RequireAvatarAddress(); + + var season = await _seasonRepo.GetSeasonAsync(seasonId); + + if (season is not null && season.IsActivated) { - await _participantService.AddParticipantAsync(seasonId, joinRequest); + await _participantRepo.InsertParticipantToSpecificSeasonAsync( + seasonId, + avatarAddress, + joinRequest.NameWithHash, + joinRequest.PortraitId + ); return TypedResults.Created(); } diff --git a/ArenaService/Controllers/SeasonController.cs b/ArenaService/Controllers/SeasonController.cs index 695e1be..70a9e64 100644 --- a/ArenaService/Controllers/SeasonController.cs +++ b/ArenaService/Controllers/SeasonController.cs @@ -1,7 +1,9 @@ namespace ArenaService.Controllers; using ArenaService.Dtos; -using ArenaService.Services; +using ArenaService.Extensions; +using ArenaService.Repositories; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -9,11 +11,11 @@ namespace ArenaService.Controllers; [ApiController] public class SeasonController : ControllerBase { - private readonly SeasonService _seasonService; + private readonly ISeasonRepository _seasonRepo; - public SeasonController(SeasonService seasonService) + public SeasonController(ISeasonRepository seasonRepo) { - _seasonService = seasonService; + _seasonRepo = seasonRepo; } [HttpGet("current")] @@ -21,13 +23,23 @@ public async Task, Ok>> GetCurrentSeaso int blockIndex ) { - var currentSeason = await _seasonService.GetCurrentSeasonAsync(blockIndex); + var seasons = await _seasonRepo.GetActivatedSeasonsAsync(); + var currentSeason = seasons.FirstOrDefault(s => + s.StartBlockIndex <= blockIndex && s.EndBlockIndex >= blockIndex + ); if (currentSeason == null) { return TypedResults.NotFound("No active season found."); } - return TypedResults.Ok(currentSeason); + return TypedResults.Ok(currentSeason?.ToResponse()); + } + + [HttpPost("/{id}")] + [Authorize(Roles = "Admin", AuthenticationSchemes = "ES256K")] + public async Task, Ok>> AddSeason() + { + return TypedResults.Ok(); } } diff --git a/ArenaService/Dtos/JoinRequest.cs b/ArenaService/Dtos/JoinRequest.cs index 5dbe56a..1aa45e3 100644 --- a/ArenaService/Dtos/JoinRequest.cs +++ b/ArenaService/Dtos/JoinRequest.cs @@ -2,8 +2,6 @@ namespace ArenaService.Dtos; public class JoinRequest { - public required string AvatarAddress { get; set; } public required string NameWithHash { get; set; } public required int PortraitId { get; set; } - public required string AuthToken { get; set; } } diff --git a/ArenaService/Extensions/ClaimsPrincipalExtensions.cs b/ArenaService/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..ba76653 --- /dev/null +++ b/ArenaService/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,14 @@ +using System.Security.Claims; + +namespace ArenaService.Extensions; + +public static class ClaimsPrincipalExtensions +{ + public static string RequireAvatarAddress(this ClaimsPrincipal user) + { + return user?.Claims.FirstOrDefault(c => c.Type == "avatar_address")?.Value + ?? throw new UnauthorizedAccessException( + "Avatar address is required but not provided." + ); + } +} diff --git a/ArenaService/Program.cs b/ArenaService/Program.cs index d8794f7..f88df7f 100644 --- a/ArenaService/Program.cs +++ b/ArenaService/Program.cs @@ -1,48 +1,13 @@ -using ArenaService.Data; -using ArenaService.Repositories; -using ArenaService.Services; -using Microsoft.EntityFrameworkCore; +using ArenaService; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddControllers(); -builder.Services.AddDbContext(options => - options - .UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) - .UseSnakeCaseNamingConvention() -); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddCors(options => -{ - options.AddPolicy( - "AllowAllOrigins", - builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader() - ); -}); +var startup = new Startup(builder.Configuration); +startup.ConfigureServices(builder.Services); var app = builder.Build(); -app.UseDeveloperExceptionPage(); -app.UseSwagger(); -app.UseSwaggerUI(); - -app.UseHttpsRedirection(); - -app.UseAuthorization(); - -app.UseCors("AllowAllOrigins"); - -app.MapControllers(); +var env = app.Services.GetRequiredService(); +startup.Configure(app, env); app.Run(); diff --git a/ArenaService/Services/AvailableOpponentService.cs b/ArenaService/Services/AvailableOpponentService.cs deleted file mode 100644 index 438216d..0000000 --- a/ArenaService/Services/AvailableOpponentService.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace ArenaService.Services; - -using ArenaService.Dtos; -using ArenaService.Extensions; -using ArenaService.Models; -using ArenaService.Repositories; - -public class AvailableOpponentService -{ - private readonly IAvailableOpponentRepository _availableOpponentRepository; - - public AvailableOpponentService(IAvailableOpponentRepository availableOpponentRepository) - { - _availableOpponentRepository = availableOpponentRepository; - } - - public async Task> GetAvailableOpponents(int participantId) - { - var availableOpponents = await _availableOpponentRepository.GetAvailableOpponents( - participantId - ); - var opponents = availableOpponents.Select(ao => ao.Opponent).ToList(); - return opponents; - } -} diff --git a/ArenaService/Services/ParticipaintService.cs b/ArenaService/Services/ParticipaintService.cs deleted file mode 100644 index e88e1ec..0000000 --- a/ArenaService/Services/ParticipaintService.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace ArenaService.Services; - -using ArenaService.Dtos; -using ArenaService.Extensions; -using ArenaService.Models; -using ArenaService.Repositories; - -public class ParticipantService -{ - private readonly IParticipantRepository _participantRepository; - - public ParticipantService(IParticipantRepository participantRepository) - { - _participantRepository = participantRepository; - } - - public async Task AddParticipantAsync( - int seasonId, - JoinRequest joinRequest - ) - { - var participant = await _participantRepository.InsertParticipantToSpecificSeasonAsync( - seasonId, - joinRequest.AvatarAddress, - joinRequest.NameWithHash, - joinRequest.PortraitId - ); - return participant.ToResponse(); - } - - public async Task GetParticipantByAvatarAddressAsync( - int seasonId, - string avatarAddress - ) - { - var participant = await _participantRepository.GetParticipantByAvatarAddressAsync( - seasonId, - avatarAddress - ); - return participant; - } -} diff --git a/ArenaService/Services/SeasonService.cs b/ArenaService/Services/SeasonService.cs deleted file mode 100644 index 8cd439d..0000000 --- a/ArenaService/Services/SeasonService.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace ArenaService.Services; - -using ArenaService.Dtos; -using ArenaService.Extensions; -using ArenaService.Repositories; - -public class SeasonService -{ - private readonly ISeasonRepository _seasonRepository; - - public SeasonService(ISeasonRepository seasonRepository) - { - _seasonRepository = seasonRepository; - } - - public async Task IsActivatedSeason(int seasonId) - { - var season = await _seasonRepository.GetSeasonAsync(seasonId); - - if (season == null) - { - return false; - } - - return season.IsActivated; - } - - public async Task GetCurrentSeasonAsync(int blockIndex) - { - var seasons = await _seasonRepository.GetActivatedSeasonsAsync(); - var currentSeason = seasons.FirstOrDefault(s => - s.StartBlockIndex <= blockIndex && s.EndBlockIndex >= blockIndex - ); - - return currentSeason?.ToResponse(); - } -} diff --git a/ArenaService/Setup.cs b/ArenaService/Setup.cs new file mode 100644 index 0000000..c79d22e --- /dev/null +++ b/ArenaService/Setup.cs @@ -0,0 +1,107 @@ +namespace ArenaService; + +using ArenaService.Auth; +using ArenaService.Data; +using ArenaService.Repositories; +using Microsoft.AspNetCore.Authentication; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; + +public class Startup +{ + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + services + .AddAuthentication("ES256K") + .AddScheme("ES256K", null); + + services.AddDbContext(options => + options + .UseNpgsql(Configuration.GetConnectionString("DefaultConnection")) + .UseSnakeCaseNamingConvention() + ); + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + + services.AddSwaggerGen(options => + { + options.SwaggerDoc( + "v1", + new OpenApiInfo { Title = "ArenaService API", Version = "v1" } + ); + + options.AddSecurityDefinition( + "Bearer", + new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = + "Enter 'Bearer' followed by a space and the JWT. Example: 'Bearer your-token'" + } + ); + + options.AddSecurityRequirement( + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + } + ); + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddCors(options => + { + options.AddPolicy( + "AllowAllOrigins", + builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader() + ); + }); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(); + + app.UseHttpsRedirection(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseCors("AllowAllOrigins"); + app.UseRouting(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +}