From aad9e473ba56ee3b2af04d9a122d72af44c17063 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:08:13 +0000 Subject: [PATCH] Add maintenance controller (#642) - Add missing test + fixes for zena functionality --- .../Features/Maintenance/MaintenanceTest.cs | 92 +++++++++++++++++++ .../Features/Zena/ZenaTest.cs | 86 +++++++++++++++++ DragaliaAPI.Integration.Test/TestFixture.cs | 49 ++++++---- .../BypassResourceVersionCheckAttribute.cs | 6 -- .../Controllers/Dragalia/DeployController.cs | 3 +- .../Controllers/Dragalia/EulaController.cs | 3 +- .../Controllers/Dragalia/ToolController.cs | 27 ++---- .../Controllers/DragaliaControllerBase.cs | 22 ++++- .../Maintenance/MaintenanceController.cs | 27 ++++++ .../Maintenance/MaintenanceService.cs | 32 +++++++ .../Features/Version/VersionController.cs | 3 +- .../Features/Zena/GetTeamDataResponse.cs | 23 ++--- DragaliaAPI/Features/Zena/IZenaService.cs | 2 +- DragaliaAPI/Features/Zena/ZenaController.cs | 14 +-- DragaliaAPI/Features/Zena/ZenaService.cs | 43 ++++++--- .../Middleware/MaintenanceActionFilter.cs | 35 +++++++ .../Middleware/NotFoundHandlerMiddleware.cs | 28 +++--- .../Middleware/ResourceVersionActionFilter.cs | 11 --- .../Models/Options/MaintenanceOptions.cs | 12 +++ DragaliaAPI/Program.cs | 3 +- DragaliaAPI/ServiceConfiguration.cs | 7 +- DragaliaAPI/appsettings.json | 6 ++ 22 files changed, 422 insertions(+), 112 deletions(-) create mode 100644 DragaliaAPI.Integration.Test/Features/Maintenance/MaintenanceTest.cs create mode 100644 DragaliaAPI.Integration.Test/Features/Zena/ZenaTest.cs delete mode 100644 DragaliaAPI/Controllers/BypassResourceVersionCheckAttribute.cs create mode 100644 DragaliaAPI/Features/Maintenance/MaintenanceController.cs create mode 100644 DragaliaAPI/Features/Maintenance/MaintenanceService.cs create mode 100644 DragaliaAPI/Middleware/MaintenanceActionFilter.cs create mode 100644 DragaliaAPI/Models/Options/MaintenanceOptions.cs diff --git a/DragaliaAPI.Integration.Test/Features/Maintenance/MaintenanceTest.cs b/DragaliaAPI.Integration.Test/Features/Maintenance/MaintenanceTest.cs new file mode 100644 index 000000000..86aab42d4 --- /dev/null +++ b/DragaliaAPI.Integration.Test/Features/Maintenance/MaintenanceTest.cs @@ -0,0 +1,92 @@ +using DragaliaAPI.Models.Options; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace DragaliaAPI.Integration.Test.Features.Maintenance; + +public class MaintenanceTest : TestFixture +{ + private readonly CustomWebApplicationFactory factory; + + public MaintenanceTest(CustomWebApplicationFactory factory, ITestOutputHelper testOutputHelper) + : base(factory, testOutputHelper) + { + this.factory = factory; + } + + [Fact] + public async Task MaintenanceActive_ReturnsResultCode() + { + this.ConfigureMaintenanceClient(new MaintenanceOptions() { Enabled = true }); + + DragaliaResponse response = await this.Client.PostMsgpack( + "load/index", + new LoadIndexRequest(), + ensureSuccessHeader: false + ); + + response.data_headers.result_code.Should().Be(ResultCode.CommonMaintenance); + } + + [Fact] + public async Task MaintenanceActive_CoreEndpoint_ReturnsNormalResponse() + { + this.ConfigureMaintenanceClient(new MaintenanceOptions() { Enabled = true }); + + DragaliaResponse response = + await this.Client.PostMsgpack( + "tool/get_service_status", + new ToolGetServiceStatusRequest(), + ensureSuccessHeader: false + ); + + response.data_headers.result_code.Should().Be(ResultCode.Success); + response.data.service_status.Should().Be(1); + } + + [Fact] + public async Task MaintenanceActive_GetText_ReturnsText() + { + this.ConfigureMaintenanceClient( + new MaintenanceOptions() + { + Enabled = true, + Title = "Title", + Body = "Body", + End = DateTimeOffset.UnixEpoch + } + ); + + DragaliaResponse response = + await this.Client.PostMsgpack( + "maintenance/get_text", + new MaintenanceGetTextRequest() + ); + + response + .data.maintenance_text.Should() + .BeEquivalentTo( + $""" + Title + Body + Check back at: + 1970-01-01T09:00:00 + """ // Date must be in Japan Standard Time + ); + } + + private void ConfigureMaintenanceClient(MaintenanceOptions options) => + this.Client = this.CreateClient(builder => + builder.ConfigureTestServices(services => + services.Configure(opts => + { + opts.Enabled = options.Enabled; + opts.Title = options.Title; + opts.Body = options.Body; + opts.End = options.End; + }) + ) + ); +} diff --git a/DragaliaAPI.Integration.Test/Features/Zena/ZenaTest.cs b/DragaliaAPI.Integration.Test/Features/Zena/ZenaTest.cs new file mode 100644 index 000000000..be16f9ac2 --- /dev/null +++ b/DragaliaAPI.Integration.Test/Features/Zena/ZenaTest.cs @@ -0,0 +1,86 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using DragaliaAPI.Features.Zena; + +namespace DragaliaAPI.Integration.Test.Features.Zena; + +public class ZenaTest : TestFixture +{ + public ZenaTest(CustomWebApplicationFactory factory, ITestOutputHelper testOutputHelper) + : base(factory, testOutputHelper) + { + Environment.SetEnvironmentVariable("ZENA_TOKEN", "token"); + + this.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Bearer", + "token" + ); + } + + [Fact] + public async Task GetTeamData_ValidId_ReturnsTeamData() + { + HttpResponseMessage zenaResponse = await this.Client.GetAsync( + $"zena/get_team_data?id={this.ViewerId}&teamnum=1" + ); + + zenaResponse.Should().BeSuccessful(); + + GetTeamDataResponse? deserialized = + await zenaResponse.Content.ReadFromJsonAsync(); + + deserialized + .Should() + .BeEquivalentTo( + new GetTeamDataResponse() + { + Name = "Euden", + Unit1 = Charas.ThePrince, + Unit2 = Charas.Empty, + Unit3 = Charas.Empty, + Unit4 = Charas.Empty, + } + ); + } + + [Fact] + public async Task GetTeamData_ValidId_MultiTeam_ReturnsTeamData() + { + HttpResponseMessage zenaResponse = await this.Client.GetAsync( + $"zena/get_team_data?id={this.ViewerId}&teamnum=1&teamnum2=2" + ); + + zenaResponse.Should().BeSuccessful(); + + GetTeamDataResponse? deserialized = + await zenaResponse.Content.ReadFromJsonAsync(); + + deserialized + .Should() + .BeEquivalentTo( + new GetTeamDataResponse() + { + Name = "Euden", + Unit1 = Charas.ThePrince, + Unit2 = Charas.Empty, + Unit3 = Charas.Empty, + Unit4 = Charas.Empty, + Unit5 = Charas.ThePrince, + Unit6 = Charas.Empty, + Unit7 = Charas.Empty, + Unit8 = Charas.Empty, + } + ); + } + + [Fact] + public async Task GetTeamData_InvalidId_Returns404() + { + HttpResponseMessage zenaResponse = await this.Client.GetAsync( + "zena/get_team_data?id=9999&teamnum=1&teamnum2=2" + ); + + zenaResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + } +} diff --git a/DragaliaAPI.Integration.Test/TestFixture.cs b/DragaliaAPI.Integration.Test/TestFixture.cs index 86585f9f8..843ed79fd 100644 --- a/DragaliaAPI.Integration.Test/TestFixture.cs +++ b/DragaliaAPI.Integration.Test/TestFixture.cs @@ -30,7 +30,7 @@ public class TestFixture : IClassFixture, IAsyncLif /// /// The session ID which is associated with the logged in test user. /// - private const string SessionId = "session_id"; + protected const string SessionId = "session_id"; private readonly CustomWebApplicationFactory factory; @@ -39,24 +39,7 @@ protected TestFixture(CustomWebApplicationFactory factory, ITestOutputHelper tes this.factory = factory; this.TestOutputHelper = testOutputHelper; - this.Client = factory - .WithWebHostBuilder(builder => - builder.ConfigureLogging(logging => - { - logging.ClearProviders(); - logging.AddXUnit(this.TestOutputHelper); - }) - ) - .CreateClient( - new WebApplicationFactoryClientOptions() - { - BaseAddress = new Uri("http://localhost/api/", UriKind.Absolute), - } - ); - - this.Client.DefaultRequestHeaders.Add("SID", SessionId); - this.Client.DefaultRequestHeaders.Add("Platform", "2"); - this.Client.DefaultRequestHeaders.Add("Res-Ver", "y2XM6giU6zz56wCm"); + this.Client = this.CreateClient(); this.MockBaasApi.Setup(x => x.GetKeys()).ReturnsAsync(TokenHelper.SecurityKeys); this.MockDateTimeProvider.SetupGet(x => x.UtcNow).Returns(() => DateTimeOffset.UtcNow); @@ -91,7 +74,7 @@ protected TestFixture(CustomWebApplicationFactory factory, ITestOutputHelper tes /// protected long ViewerId { get; private set; } - protected HttpClient Client { get; } + protected HttpClient Client { get; set; } protected IMapper Mapper { get; } @@ -189,6 +172,32 @@ protected long GetDragonKeyId(Dragons dragon) .First(); } + protected HttpClient CreateClient(Action? extraBuilderConfig = null) + { + HttpClient client = factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddXUnit(this.TestOutputHelper); + }); + extraBuilderConfig?.Invoke(builder); + }) + .CreateClient( + new WebApplicationFactoryClientOptions() + { + BaseAddress = new Uri("http://localhost/api/", UriKind.Absolute), + } + ); + + client.DefaultRequestHeaders.Add("SID", SessionId); + client.DefaultRequestHeaders.Add("Platform", "2"); + client.DefaultRequestHeaders.Add("Res-Ver", "y2XM6giU6zz56wCm"); + + return client; + } + protected long GetTalismanKeyId(Talismans talisman) { return this.ApiContext.PlayerTalismans.Where(x => x.TalismanId == talisman) diff --git a/DragaliaAPI/Controllers/BypassResourceVersionCheckAttribute.cs b/DragaliaAPI/Controllers/BypassResourceVersionCheckAttribute.cs deleted file mode 100644 index a8b2806ad..000000000 --- a/DragaliaAPI/Controllers/BypassResourceVersionCheckAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DragaliaAPI.Controllers; - -/// -/// Causes not to execute. -/// -public class BypassResourceVersionCheckAttribute : Attribute { } diff --git a/DragaliaAPI/Controllers/Dragalia/DeployController.cs b/DragaliaAPI/Controllers/Dragalia/DeployController.cs index 9be1158a0..2e195ae0e 100644 --- a/DragaliaAPI/Controllers/Dragalia/DeployController.cs +++ b/DragaliaAPI/Controllers/Dragalia/DeployController.cs @@ -6,8 +6,7 @@ namespace DragaliaAPI.Controllers.Dragalia; [Route("deploy")] [AllowAnonymous] -[BypassResourceVersionCheck] -public class DeployController : DragaliaControllerBase +public class DeployController : DragaliaControllerBaseCore { private const string DeployHash = "13bb2827ce9e6a66015ac2808112e3442740e862"; diff --git a/DragaliaAPI/Controllers/Dragalia/EulaController.cs b/DragaliaAPI/Controllers/Dragalia/EulaController.cs index 14a536a8e..3fabcc170 100644 --- a/DragaliaAPI/Controllers/Dragalia/EulaController.cs +++ b/DragaliaAPI/Controllers/Dragalia/EulaController.cs @@ -6,8 +6,7 @@ namespace DragaliaAPI.Controllers.Dragalia; [Route("eula")] [AllowAnonymous] -[BypassResourceVersionCheck] -public class EulaController : DragaliaControllerBase +public class EulaController : DragaliaControllerBaseCore { private static readonly List AllEulaVersions = new() diff --git a/DragaliaAPI/Controllers/Dragalia/ToolController.cs b/DragaliaAPI/Controllers/Dragalia/ToolController.cs index 04844dbfc..6e9bf6381 100644 --- a/DragaliaAPI/Controllers/Dragalia/ToolController.cs +++ b/DragaliaAPI/Controllers/Dragalia/ToolController.cs @@ -1,35 +1,24 @@ using DragaliaAPI.Models.Generated; +using DragaliaAPI.Models.Options; using DragaliaAPI.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; namespace DragaliaAPI.Controllers.Dragalia; -/// -/// This is presumably used to create a savefile on Dragalia's servers, -/// but we do that after creating a DeviceAccount in the Nintendo endpoint, -/// because we aren't limited by having two separate servers/DBs. -/// -/// As a result, this controller just retrieves the existing savefile and -/// responds with its viewer_id. -/// [Route("tool")] [AllowAnonymous] -[BypassResourceVersionCheck] -public class ToolController : DragaliaControllerBase +public class ToolController(IAuthService authService) : DragaliaControllerBaseCore { - private readonly IAuthService authService; - - public ToolController(IAuthService authService) - { - this.authService = authService; - } + private const int OkServiceStatus = 1; + private const int MaintenanceServiceStatus = 2; [HttpPost] [Route("signup")] public async Task Signup([FromHeader(Name = "ID-TOKEN")] string idToken) { - (long viewerId, _) = await this.authService.DoAuth(idToken); + (long viewerId, _) = await authService.DoAuth(idToken); return this.Ok( new ToolSignupData() @@ -54,7 +43,7 @@ public async Task Auth([FromHeader(Name = "ID-TOKEN")] string id // For some reason, the id_token in the ToolAuthRequest does not update with refreshes, // but the one in the header does. - (long viewerId, string sessionId) = await this.authService.DoAuth(idToken); + (long viewerId, string sessionId) = await authService.DoAuth(idToken); return this.Ok( new ToolAuthData() @@ -69,7 +58,7 @@ public async Task Auth([FromHeader(Name = "ID-TOKEN")] string id [HttpPost("reauth")] public async Task Reauth([FromHeader(Name = "ID-TOKEN")] string idToken) { - (long viewerId, string sessionId) = await this.authService.DoAuth(idToken); + (long viewerId, string sessionId) = await authService.DoAuth(idToken); return this.Ok( new ToolReauthData() diff --git a/DragaliaAPI/Controllers/DragaliaControllerBase.cs b/DragaliaAPI/Controllers/DragaliaControllerBase.cs index 4a1e87abe..16261f0c0 100644 --- a/DragaliaAPI/Controllers/DragaliaControllerBase.cs +++ b/DragaliaAPI/Controllers/DragaliaControllerBase.cs @@ -7,13 +7,20 @@ namespace DragaliaAPI.Controllers; +/// +/// Defines a controller for all Dawnshard API endpoints that implements the required metadata, +/// and which provides helpers to serialize Dragalia responses. +/// +/// +/// For most controllers that are not involved in the title screen, +/// should be used. +/// [ApiController] [SerializeException] -[ServiceFilter(typeof(ResourceVersionActionFilter))] [Authorize(AuthenticationSchemes = SchemeName.Session)] [Consumes("application/octet-stream")] [Produces("application/x-msgpack")] -public abstract class DragaliaControllerBase : ControllerBase +public abstract class DragaliaControllerBaseCore : ControllerBase { protected string DeviceAccountId => this.User.FindFirstValue(CustomClaimType.AccountId) @@ -50,3 +57,14 @@ public OkObjectResult Code(ResultCode code) return this.Code(code, string.Empty); } } + +/// +/// Extends with extra action filters that can return the player to the title +/// screen under certain circumstances. +/// +/// +/// Not to be used for endpoints that make up the title screen (/tool/*, /version/*, etc.) to prevent infinite loops. +/// +[ServiceFilter] +[ServiceFilter] +public abstract class DragaliaControllerBase : DragaliaControllerBaseCore { } diff --git a/DragaliaAPI/Features/Maintenance/MaintenanceController.cs b/DragaliaAPI/Features/Maintenance/MaintenanceController.cs new file mode 100644 index 000000000..42d1f6b4a --- /dev/null +++ b/DragaliaAPI/Features/Maintenance/MaintenanceController.cs @@ -0,0 +1,27 @@ +using DragaliaAPI.Controllers; +using DragaliaAPI.Models.Generated; +using Microsoft.AspNetCore.Mvc; + +namespace DragaliaAPI.Features.Maintenance; + +[Route("maintenance")] +public class MaintenanceController( + MaintenanceService maintenanceService, + ILogger logger +) : DragaliaControllerBaseCore +{ + [HttpPost("get_text")] + public DragaliaResult GetText(MaintenanceGetTextRequest request) + { + if (!maintenanceService.CheckIsMaintenance()) + { + logger.LogError("Invalid call to get maintenance text: maintenance is not active"); + return this.Code(ResultCode.CommonServerError); + } + + return new MaintenanceGetTextData() + { + maintenance_text = maintenanceService.GetMaintenanceText() + }; + } +} diff --git a/DragaliaAPI/Features/Maintenance/MaintenanceService.cs b/DragaliaAPI/Features/Maintenance/MaintenanceService.cs new file mode 100644 index 000000000..68a0228d3 --- /dev/null +++ b/DragaliaAPI/Features/Maintenance/MaintenanceService.cs @@ -0,0 +1,32 @@ +using DragaliaAPI.Models.Options; +using Microsoft.Extensions.Options; + +namespace DragaliaAPI.Features.Maintenance; + +public class MaintenanceService(IOptionsMonitor maintenanceOptions) +{ + public bool CheckIsMaintenance() => maintenanceOptions.CurrentValue.Enabled; + + public string GetMaintenanceText() + { + string date = string.Empty; + string schedule = string.Empty; + + if (maintenanceOptions.CurrentValue.End is not null) + { + DateTime japanStandardTimeDate = + maintenanceOptions.CurrentValue.End.Value.UtcDateTime.AddHours(9); + date = japanStandardTimeDate.ToString("s"); + schedule = "Check back at:"; + } + + string xml = $""" + {maintenanceOptions.CurrentValue.Title} + {maintenanceOptions.CurrentValue.Body} + {schedule} + {date} + """; + + return xml; + } +} diff --git a/DragaliaAPI/Features/Version/VersionController.cs b/DragaliaAPI/Features/Version/VersionController.cs index 20a766367..ba8d445f3 100644 --- a/DragaliaAPI/Features/Version/VersionController.cs +++ b/DragaliaAPI/Features/Version/VersionController.cs @@ -7,9 +7,8 @@ namespace DragaliaAPI.Features.Version; [Route("version")] [AllowAnonymous] -[BypassResourceVersionCheck] public class VersionController(IResourceVersionService resourceVersionService) - : DragaliaControllerBase + : DragaliaControllerBaseCore { [HttpPost] [Route("get_resource_version")] diff --git a/DragaliaAPI/Features/Zena/GetTeamDataResponse.cs b/DragaliaAPI/Features/Zena/GetTeamDataResponse.cs index 2a857fbb8..e298dbbc6 100644 --- a/DragaliaAPI/Features/Zena/GetTeamDataResponse.cs +++ b/DragaliaAPI/Features/Zena/GetTeamDataResponse.cs @@ -2,14 +2,15 @@ namespace DragaliaAPI.Features.Zena; -public record struct GetTeamDataResponse( - string Name, - Charas Unit1, - Charas Unit2, - Charas Unit3, - Charas Unit4, - Charas? Unit5, - Charas? Unit6, - Charas? Unit7, - Charas? Unit8 -); +public class GetTeamDataResponse +{ + public required string Name { get; set; } + public Charas Unit1 { get; set; } + public Charas Unit2 { get; set; } + public Charas Unit3 { get; set; } + public Charas Unit4 { get; set; } + public Charas? Unit5 { get; set; } + public Charas? Unit6 { get; set; } + public Charas? Unit7 { get; set; } + public Charas? Unit8 { get; set; } +} diff --git a/DragaliaAPI/Features/Zena/IZenaService.cs b/DragaliaAPI/Features/Zena/IZenaService.cs index 0c2c7c21e..e07d63b22 100644 --- a/DragaliaAPI/Features/Zena/IZenaService.cs +++ b/DragaliaAPI/Features/Zena/IZenaService.cs @@ -2,5 +2,5 @@ public interface IZenaService { - Task GetTeamData(IEnumerable partyNumbers); + Task GetTeamData(IEnumerable partyNumbers); } diff --git a/DragaliaAPI/Features/Zena/ZenaController.cs b/DragaliaAPI/Features/Zena/ZenaController.cs index ebe6ebdfe..948cbdb00 100644 --- a/DragaliaAPI/Features/Zena/ZenaController.cs +++ b/DragaliaAPI/Features/Zena/ZenaController.cs @@ -10,11 +10,8 @@ namespace DragaliaAPI.Features.Zena; public class ZenaController(IPlayerIdentityService playerIdentityService, IZenaService zenaService) : ControllerBase { - private readonly IPlayerIdentityService playerIdentityService = playerIdentityService; - private readonly IZenaService zenaService = zenaService; - [HttpGet("get_team_data")] - public async Task GetTeamData( + public async Task> GetTeamData( [FromQuery] long id, [FromQuery] int teamnum, [FromQuery] int teamnum2 @@ -24,8 +21,13 @@ [FromQuery] int teamnum2 if (teamnum2 != -1) teamNumbers.Add(teamnum2); - using IDisposable impersonation = this.playerIdentityService.StartUserImpersonation(id); + using IDisposable impersonation = playerIdentityService.StartUserImpersonation(id); + + GetTeamDataResponse? response = await zenaService.GetTeamData(teamNumbers); + + if (response is null) + return this.NotFound(); - return await this.zenaService.GetTeamData(teamNumbers); + return response; } } diff --git a/DragaliaAPI/Features/Zena/ZenaService.cs b/DragaliaAPI/Features/Zena/ZenaService.cs index 8047d728c..db508d592 100644 --- a/DragaliaAPI/Features/Zena/ZenaService.cs +++ b/DragaliaAPI/Features/Zena/ZenaService.cs @@ -5,23 +5,38 @@ namespace DragaliaAPI.Features.Zena; -public class ZenaService(IPlayerIdentityService playerIdentityService, ApiContext apiContext) - : IZenaService +public class ZenaService( + IPlayerIdentityService playerIdentityService, + ApiContext apiContext, + ILogger logger +) : IZenaService { private readonly IPlayerIdentityService playerIdentityService = playerIdentityService; private readonly ApiContext apiContext = apiContext; - public async Task GetTeamData(IEnumerable partyNumbers) + public async Task GetTeamData(IEnumerable partyNumbers) { - string playerName = await this.apiContext.PlayerUserData.Where(x => + string? playerName = await this.apiContext.PlayerUserData.Where(x => x.ViewerId == this.playerIdentityService.ViewerId ) .Select(x => x.Name) - .FirstAsync(); + .FirstOrDefaultAsync(); + + if (playerName is null) + { + logger.LogWarning( + "Failed to get team data: player with ID {ViewerId} does not exist.", + this.playerIdentityService.ViewerId + ); + + return null; + } Charas[] charas = await this.apiContext.PlayerPartyUnits.Where(x => x.ViewerId == this.playerIdentityService.ViewerId && partyNumbers.Contains(x.PartyNo) ) + .OrderBy(x => x.PartyNo) + .ThenBy(x => x.UnitNo) .Select(x => x.CharaId) .ToArrayAsync(); @@ -40,16 +55,14 @@ public async Task GetTeamData(IEnumerable partyNumbers Unit8 = charas.ElementAtOrDefault(7), }; } - else + + return new() { - return new() - { - Name = playerName, - Unit1 = charas.ElementAtOrDefault(0), - Unit2 = charas.ElementAtOrDefault(1), - Unit3 = charas.ElementAtOrDefault(2), - Unit4 = charas.ElementAtOrDefault(3), - }; - } + Name = playerName, + Unit1 = charas.ElementAtOrDefault(0), + Unit2 = charas.ElementAtOrDefault(1), + Unit3 = charas.ElementAtOrDefault(2), + Unit4 = charas.ElementAtOrDefault(3), + }; } } diff --git a/DragaliaAPI/Middleware/MaintenanceActionFilter.cs b/DragaliaAPI/Middleware/MaintenanceActionFilter.cs new file mode 100644 index 000000000..79ce6193d --- /dev/null +++ b/DragaliaAPI/Middleware/MaintenanceActionFilter.cs @@ -0,0 +1,35 @@ +using DragaliaAPI.Models; +using DragaliaAPI.Models.Options; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; + +namespace DragaliaAPI.Middleware; + +[UsedImplicitly] +public class MaintenanceActionFilter( + IOptionsMonitor options, + ILogger logger +) : IActionFilter +{ + private readonly IOptionsMonitor options = options; + private readonly ILogger logger = logger; + + public void OnActionExecuting(ActionExecutingContext context) + { + if (!this.options.CurrentValue.Enabled) + return; + + this.logger.LogInformation("Aborting request due to active maintenance."); + + context.Result = new OkObjectResult( + new DragaliaResponse( + data_headers: new DataHeaders(ResultCode.CommonMaintenance), + new ResultCodeData(ResultCode.CommonMaintenance) + ) + ); + } + + public void OnActionExecuted(ActionExecutedContext context) { } +} diff --git a/DragaliaAPI/Middleware/NotFoundHandlerMiddleware.cs b/DragaliaAPI/Middleware/NotFoundHandlerMiddleware.cs index 693071f49..bb9cde3f4 100644 --- a/DragaliaAPI/Middleware/NotFoundHandlerMiddleware.cs +++ b/DragaliaAPI/Middleware/NotFoundHandlerMiddleware.cs @@ -22,17 +22,21 @@ public async Task InvokeAsync(HttpContext context) { await next(context); - if (context.Response.StatusCode == (int)HttpStatusCode.NotFound) - { - this.logger.LogInformation("HTTP 404 on {RequestPath}", context.Request.Path); - context.Response.StatusCode = (int)HttpStatusCode.OK; - - DragaliaResponse gameResponse = - new(new DataHeaders(NotFoundCode), new(NotFoundCode)); - - await context.Response.Body.WriteAsync( - MessagePackSerializer.Serialize(gameResponse, CustomResolver.Options) - ); - } + if (context.Response.StatusCode != (int)HttpStatusCode.NotFound) + return; + + if (context.GetEndpoint() is not null) + // Exclude controllers where we return this.NotFound() explicitly + return; + + this.logger.LogInformation("HTTP 404 on {RequestPath}", context.Request.Path); + context.Response.StatusCode = (int)HttpStatusCode.OK; + + DragaliaResponse gameResponse = + new(new DataHeaders(NotFoundCode), new(NotFoundCode)); + + await context.Response.Body.WriteAsync( + MessagePackSerializer.Serialize(gameResponse, CustomResolver.Options) + ); } } diff --git a/DragaliaAPI/Middleware/ResourceVersionActionFilter.cs b/DragaliaAPI/Middleware/ResourceVersionActionFilter.cs index 4c99fcdb7..e69af9914 100644 --- a/DragaliaAPI/Middleware/ResourceVersionActionFilter.cs +++ b/DragaliaAPI/Middleware/ResourceVersionActionFilter.cs @@ -13,17 +13,6 @@ ILogger logger { public void OnActionExecuting(ActionExecutingContext context) { - if ( - context - .HttpContext.GetEndpoint() - ?.Metadata - .GetMetadata() - is not null - ) - { - return; - } - string? clientResourceVer = context.HttpContext.Request.Headers["Res-Ver"].FirstOrDefault(); if (clientResourceVer is null) return; diff --git a/DragaliaAPI/Models/Options/MaintenanceOptions.cs b/DragaliaAPI/Models/Options/MaintenanceOptions.cs new file mode 100644 index 000000000..05cccdbbf --- /dev/null +++ b/DragaliaAPI/Models/Options/MaintenanceOptions.cs @@ -0,0 +1,12 @@ +namespace DragaliaAPI.Models.Options; + +public class MaintenanceOptions +{ + public bool Enabled { get; set; } + + public string? Title { get; set; } + + public string? Body { get; set; } + + public DateTimeOffset? End { get; set; } +} diff --git a/DragaliaAPI/Program.cs b/DragaliaAPI/Program.cs index ad9adfc38..8da95ad04 100644 --- a/DragaliaAPI/Program.cs +++ b/DragaliaAPI/Program.cs @@ -50,7 +50,8 @@ .Configure(config.GetRequiredSection(nameof(TimeAttackOptions))) .Configure(config.GetRequiredSection(nameof(ResourceVersionOptions))) .Configure(config.GetRequiredSection(nameof(BlazorOptions))) - .Configure(config.GetRequiredSection(nameof(EventOptions))); + .Configure(config.GetRequiredSection(nameof(EventOptions))) + .Configure(config.GetRequiredSection(nameof(MaintenanceOptions))); builder.Services.AddServerSideBlazor(); builder.Services.AddMudServices(options => diff --git a/DragaliaAPI/ServiceConfiguration.cs b/DragaliaAPI/ServiceConfiguration.cs index 5d6fb5cde..d616b727d 100644 --- a/DragaliaAPI/ServiceConfiguration.cs +++ b/DragaliaAPI/ServiceConfiguration.cs @@ -13,6 +13,7 @@ using DragaliaAPI.Features.Fort; using DragaliaAPI.Features.Item; using DragaliaAPI.Features.Login; +using DragaliaAPI.Features.Maintenance; using DragaliaAPI.Features.Missions; using DragaliaAPI.Features.PartyPower; using DragaliaAPI.Features.Player; @@ -149,7 +150,9 @@ IConfiguration configuration .AddScoped() .AddScoped() // Zena feature - .AddScoped(); + .AddScoped() + // Maintenance feature + .AddScoped(); services.AddScoped(); @@ -170,7 +173,7 @@ IConfiguration configuration }); services.AddScoped(); - services.AddScoped(); + services.AddScoped().AddScoped(); return services; } diff --git a/DragaliaAPI/appsettings.json b/DragaliaAPI/appsettings.json index cb89122b9..7c66e1fcd 100644 --- a/DragaliaAPI/appsettings.json +++ b/DragaliaAPI/appsettings.json @@ -104,5 +104,11 @@ }, "BlazorOptions": { "BaseImagePath": null + }, + "MaintenanceOptions": { + "Enabled": false, + "Title": "Maintenance", + "Body": "Dawnshard is currently under maintenance\nto upgrade the server.", + "End": "2022-12-01T14:00:00.000Z" } }