Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show BaaS username in account management dialog #1098

Merged
merged 4 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions DragaliaAPI/DragaliaAPI.Integration.Test/Dragalia/UserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,16 @@ public async Task LinkedNAccount_ReturnsExpectedResponse()
[Fact]
public async Task GetNAccountInfo_ReturnsExpectedResponse()
{
this.MockBaasApi.Setup(x => x.GetUsername(It.IsAny<string>())).ReturnsAsync("okada");

(await this.Client.PostMsgpack<UserGetNAccountInfoResponse>("/user/get_n_account_info"))
.Data.Should()
.BeEquivalentTo(
new UserGetNAccountInfoResponse()
{
NAccountInfo = new()
{
Email = "[email protected]",
Nickname = "placeholder nickname",
},
NAccountInfo = new() { Email = "", Nickname = "okada" },
UpdateDataList = new(),
},
opts => opts.Excluding(x => x.UpdateDataList.UserData.Crystal)
}
);
}
}
21 changes: 7 additions & 14 deletions DragaliaAPI/DragaliaAPI.Test/Helpers/BaasRequestHelperTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq.Protected;

Expand All @@ -15,30 +16,25 @@ namespace DragaliaAPI.Test.Helpers;
public class BaasRequestHelperTest
{
private readonly IBaasApi baasRequestHelper;
private readonly Mock<IOptionsMonitor<BaasOptions>> mockOptions;
private readonly Mock<HttpMessageHandler> mockHttpMessageHandler;
private readonly Mock<ILogger<BaasApi>> mockLogger;
private IDistributedCache cache;
private readonly IDistributedCache cache;

public BaasRequestHelperTest()
{
this.mockOptions = new(MockBehavior.Strict);
this.mockHttpMessageHandler = new(MockBehavior.Strict);
this.mockLogger = new(MockBehavior.Loose);

this.mockOptions.SetupGet(x => x.CurrentValue)
.Returns(new BaasOptions() { BaasUrl = "https://www.taylorswift.com/" });

IOptions<MemoryDistributedCacheOptions> opts = Options.Create(
new MemoryDistributedCacheOptions()
);
this.cache = new MemoryDistributedCache(opts);

this.baasRequestHelper = new BaasApi(
mockOptions.Object,
new HttpClient(mockHttpMessageHandler.Object),
new HttpClient(mockHttpMessageHandler.Object)
{
BaseAddress = new Uri("https://www.taylorswift.com"),
},
this.cache,
mockLogger.Object
NullLogger<BaasApi>.Instance
);
}

Expand Down Expand Up @@ -81,7 +77,6 @@ public async Task GetKeys_Success_ReturnsSecurityKey()

(await this.baasRequestHelper.GetKeys()).Should().ContainSingle();

this.mockOptions.VerifyAll();
this.mockHttpMessageHandler.VerifyAll();
}

Expand Down Expand Up @@ -110,7 +105,6 @@ public async Task GetKeys_Cached_UsesCache()

(await this.baasRequestHelper.GetKeys()).Should().ContainSingle();

this.mockOptions.VerifyAll();
this.mockHttpMessageHandler.VerifyAll();
}

Expand All @@ -133,7 +127,6 @@ await this
.ThrowExactlyAsync<DragaliaException>()
.Where(x => x.Code == ResultCode.CommonAuthError);

this.mockOptions.VerifyAll();
this.mockHttpMessageHandler.VerifyAll();
}

Expand Down
55 changes: 23 additions & 32 deletions DragaliaAPI/DragaliaAPI/Controllers/Dragalia/UserController.cs
Original file line number Diff line number Diff line change
@@ -1,61 +1,52 @@
using AutoMapper;
using DragaliaAPI.Database.Entities;
using DragaliaAPI.Database.Repositories;
using DragaliaAPI.Infrastructure;
using DragaliaAPI.Models.Generated;
using DragaliaAPI.Services;
using DragaliaAPI.Services.Api;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace DragaliaAPI.Controllers.Dragalia;

[Route("user")]
public class UserController : DragaliaControllerBase
public class UserController(
IUserDataRepository userDataRepository,
IUpdateDataService updateDataService,
IBaasApi baasApi,
ISessionService sessionService
) : DragaliaControllerBase
{
private readonly IUserDataRepository userDataRepository;
private readonly IUpdateDataService updateDataService;
private readonly IMapper mapper;

public UserController(
IUserDataRepository userDataRepository,
IUpdateDataService updateDataService,
IMapper mapper
)
{
this.userDataRepository = userDataRepository;
this.updateDataService = updateDataService;
this.mapper = mapper;
}

[HttpPost("linked_n_account")]
public async Task<DragaliaResult> LinkedNAccount(CancellationToken cancellationToken)
{
// This controller is meant to be used to set the 'Link a Nintendo Account' mission as complete
DbPlayerUserData userData = await this.userDataRepository.UserData.SingleAsync(
DbPlayerUserData userData = await userDataRepository.UserData.SingleAsync(
cancellationToken
);

userData.Crystal += 12_000;
UpdateDataList updateDataList = await this.updateDataService.SaveChangesAsync(
cancellationToken
);
UpdateDataList updateDataList = await updateDataService.SaveChangesAsync(cancellationToken);

return this.Ok(new UserLinkedNAccountResponse() { UpdateDataList = updateDataList });
}

[HttpPost("get_n_account_info")]
public DragaliaResult GetNAccountInfo()
public async Task<DragaliaResult<UserGetNAccountInfoResponse>> GetNAccountInfo(
[FromHeader(Name = DragaliaHttpConstants.Headers.SessionId)] string sessionId
)
{
// TODO: Replace this with an API call to BaaS to return actual information
return this.Ok(
new UserGetNAccountInfoResponse()
string idToken = (await sessionService.LoadSessionSessionId(sessionId)).IdToken;

return new UserGetNAccountInfoResponse()
{
NAccountInfo = new()
{
NAccountInfo = new()
{
Email = "[email protected]",
Nickname = "placeholder nickname",
},
UpdateDataList = new(),
}
);
Email = string.Empty, // This being null or empty shows the nickname instead
Nickname = await baasApi.GetUsername(idToken),
},
UpdateDataList = new(),
};
}
}
1 change: 1 addition & 0 deletions DragaliaAPI/DragaliaAPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
{
applicationBuilder.UsePathBase(prefix);
}

applicationBuilder.UseMiddleware<HeaderLogContextMiddleware>();
applicationBuilder.UseSerilogRequestLogging();
applicationBuilder.UseAuthentication();
Expand Down
11 changes: 10 additions & 1 deletion DragaliaAPI/DragaliaAPI/ServiceConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,16 @@ IConfiguration configuration

services.AddAllOfType<ISavefileUpdate>();

services.AddHttpClient<IBaasApi, BaasApi>();
services.AddHttpClient<IBaasApi, BaasApi>(
(sp, client) =>
{
IOptionsMonitor<BaasOptions> options = sp.GetRequiredService<
IOptionsMonitor<BaasOptions>
>();

client.BaseAddress = options.CurrentValue.BaasUrlParsed;
}
);

services.AddHttpClient<IPhotonStateApi, PhotonStateApi>(
(sp, client) =>
Expand Down
84 changes: 58 additions & 26 deletions DragaliaAPI/DragaliaAPI/Services/Api/BaasApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@

namespace DragaliaAPI.Services.Api;

public class BaasApi : IBaasApi
internal sealed partial class BaasApi : IBaasApi
{
private readonly IOptionsMonitor<BaasOptions> options;
private readonly HttpClient client;
private readonly IDistributedCache cache;
private readonly ILogger<BaasApi> logger;
Expand All @@ -21,19 +20,11 @@ public class BaasApi : IBaasApi
private const string SavefileEndpoint = "/gameplay/v1/savefile";
private const string RedisKey = ":jwks:baas";

public BaasApi(
IOptionsMonitor<BaasOptions> options,
HttpClient client,
IDistributedCache cache,
ILogger<BaasApi> logger
)
public BaasApi(HttpClient client, IDistributedCache cache, ILogger<BaasApi> logger)
{
this.options = options;
this.client = client;
this.cache = cache;
this.logger = logger;

this.client.BaseAddress = this.options.CurrentValue.BaasUrlParsed;
}

public async Task<IList<SecurityKey>> GetKeys()
Expand All @@ -49,7 +40,7 @@ public async Task<IList<SecurityKey>> GetKeys()

if (!keySetResponse.IsSuccessStatusCode)
{
logger.LogError("Received failure response from BaaS: {@response}", keySetResponse);
Log.ReceivedNon200Response(this.logger, KeySetEndpoint, keySetResponse.StatusCode);

throw new DragaliaException(
ResultCode.CommonAuthError,
Expand All @@ -64,16 +55,16 @@ public async Task<IList<SecurityKey>> GetKeys()
return jwks.GetSigningKeys();
}

public async Task<LoadIndexResponse> GetSavefile(string idToken)
public async Task<LoadIndexResponse> GetSavefile(string gameIdToken)
{
HttpResponseMessage savefileResponse = await client.PostAsJsonAsync<object>(
SavefileEndpoint,
new { idToken }
new { idToken = gameIdToken }
);

if (!savefileResponse.IsSuccessStatusCode)
{
logger.LogError("Received failure response from BaaS: {@response}", savefileResponse);
Log.ReceivedNon200Response(this.logger, SavefileEndpoint, savefileResponse.StatusCode);

throw new DragaliaException(
ResultCode.TransitionLinkedDataNotFound,
Expand All @@ -88,37 +79,78 @@ await savefileResponse.Content.ReadFromJsonAsync<
)?.Data ?? throw new JsonException("Deserialized savefile was null");
}

public async Task<string?> GetUserId(string idToken)
public async Task<string?> GetUserId(string webIdToken)
{
UserIdResponse? userInfo = await this.GetUserInformation<UserIdResponse>(
"/gameplay/v1/user",
webIdToken
);

return userInfo?.UserId;
}

public async Task<string?> GetUsername(string gameIdToken)
{
WebUserResponse? webUserInfo = await this.GetUserInformation<WebUserResponse>(
"/gameplay/v1/webUser",
gameIdToken
);

return webUserInfo?.Username;
}

private async Task<TResponse?> GetUserInformation<TResponse>(
string endpoint,
string bearerToken
)
where TResponse : class
{
HttpRequestMessage request =
new(HttpMethod.Get, "/gameplay/v1/user")
new(HttpMethod.Get, endpoint)
{
Headers = { Authorization = new AuthenticationHeaderValue("Bearer", idToken) },
Headers = { Authorization = new AuthenticationHeaderValue("Bearer", bearerToken) },
};

HttpResponseMessage response = await client.SendAsync(request);

if (response.StatusCode == HttpStatusCode.NotFound)
{
this.logger.LogDebug("GetUserId returned 404 Not Found.");
Log.ReceivedUserInfo404Response(this.logger, endpoint);
return null;
}

if (!response.IsSuccessStatusCode)
{
this.logger.LogError(
"Received non-200 status code in GetUserId: {Status}",
response.StatusCode
);

Log.ReceivedNon200Response(this.logger, endpoint, response.StatusCode);
return null;
}

return (await response.Content.ReadFromJsonAsync<UserIdResponse>())?.UserId;
return await response.Content.ReadFromJsonAsync<TResponse>();
}

private class UserIdResponse
{
public required string UserId { get; set; }
public required string UserId { get; init; }
}

private class WebUserResponse
{
public required string Username { get; init; }
}

private static partial class Log
{
[LoggerMessage(LogLevel.Error, "Received non-200 status code from {Endpoint}: {Status}")]
public static partial void ReceivedNon200Response(
ILogger logger,
string endpoint,
HttpStatusCode status
);

[LoggerMessage(
LogLevel.Information,
"Failed to get user information from {Endpoint}: BaaS returned 404 Not Found"
)]
public static partial void ReceivedUserInfo404Response(ILogger logger, string endpoint);
}
}
5 changes: 3 additions & 2 deletions DragaliaAPI/DragaliaAPI/Services/Api/IBaasApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace DragaliaAPI.Services.Api;
public interface IBaasApi
{
Task<IList<SecurityKey>> GetKeys();
Task<LoadIndexResponse> GetSavefile(string idToken);
Task<string?> GetUserId(string idToken);
Task<LoadIndexResponse> GetSavefile(string gameIdToken);
Task<string?> GetUserId(string webIdToken);
Task<string?> GetUsername(string gameIdToken);
}
Loading