This commit is contained in:
2025-10-07 09:28:45 +01:00
parent 3d2176693e
commit 830a401c90
3 changed files with 210 additions and 10 deletions

View File

@@ -25,6 +25,8 @@ using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using MareSynchronos.API.Dto.Account;
using MareSynchronos.MareConfiguration.Models;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
@@ -67,10 +69,18 @@ public class CompactUi : WindowMediatorSubscriberBase
private int _secretKeyIdx = -1; private int _secretKeyIdx = -1;
private bool _showModalForUserAddition; private bool _showModalForUserAddition;
private bool _wasOpen; private bool _wasOpen;
private bool _registrationInProgress = false;
private bool _registrationSuccess = false;
private string? _registrationMessage;
private RegisterReplyDto? _registrationReply;
private readonly AccountRegistrationService _registerService;
private string _secretKey = string.Empty;
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService, public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager, ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
PerformanceCollectorService performanceCollectorService) PerformanceCollectorService performanceCollectorService, AccountRegistrationService registerService)
: base(logger, mediator, "###SnowcloakSyncMainUI", performanceCollectorService) : base(logger, mediator, "###SnowcloakSyncMainUI", performanceCollectorService)
{ {
_uiSharedService = uiShared; _uiSharedService = uiShared;
@@ -78,6 +88,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_apiController = apiController; _apiController = apiController;
_pairManager = pairManager; _pairManager = pairManager;
_serverManager = serverManager; _serverManager = serverManager;
_registerService = registerService;
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_uidDisplayHandler = uidDisplayHandler; _uidDisplayHandler = uidDisplayHandler;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
@@ -109,7 +120,7 @@ public class CompactUi : WindowMediatorSubscriberBase
// changed min size // changed min size
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
MinimumSize = new Vector2(300, 400), MinimumSize = new Vector2(500, 400),
MaximumSize = new Vector2(600, 2000), MaximumSize = new Vector2(600, 2000),
}; };
} }
@@ -146,7 +157,7 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
else else
{ {
if (_uiSharedService.IconTextButton(icon, label, (150 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale)) if (_uiSharedService.IconTextButton(icon, label, (165 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale))
{ {
_selectedMenu = menu; _selectedMenu = menu;
} }
@@ -166,7 +177,7 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
else else
{ {
if (_uiSharedService.IconTextButton(icon, label, (150 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale)) if (_uiSharedService.IconTextButton(icon, label, (165 - ImGui.GetStyle().WindowPadding.X * 2) * ImGuiHelpers.GlobalScale))
{ {
onClick(); onClick();
} }
@@ -176,7 +187,8 @@ public class CompactUi : WindowMediatorSubscriberBase
{ {
// Adjust both values below to change size, 40 seems good to fit the buttons // Adjust both values below to change size, 40 seems good to fit the buttons
// 150 seems decent enough to fit the text into it, could be smaller // 150 seems decent enough to fit the text into it, could be smaller
var sidebarWidth = (_sidebarCollapsed ? 40 : 150) * ImGuiHelpers.GlobalScale; // Elf note: Adjusted to 165 since "Character Analysis" hung off the end a bit
var sidebarWidth = (_sidebarCollapsed ? 40 : 165) * ImGuiHelpers.GlobalScale;
using (var child = ImRaii.Child("Sidebar", new Vector2(sidebarWidth, -1), true)) using (var child = ImRaii.Child("Sidebar", new Vector2(sidebarWidth, -1), true))
{ {
@@ -356,10 +368,44 @@ public class CompactUi : WindowMediatorSubscriberBase
{ {
ImGui.Dummy(new(10)); ImGui.Dummy(new(10));
var keys = _serverManager.CurrentServer!.SecretKeys; var keys = _serverManager.CurrentServer!.SecretKeys;
ImGui.BeginDisabled(_registrationInProgress || _uiSharedService.ApiController.ServerState == ServerState.Connecting || _uiSharedService.ApiController.ServerState == ServerState.Reconnecting);
if (keys.Any()) if (keys.Any())
{ {
if (_secretKeyIdx == -1) _secretKeyIdx = keys.First().Key; if (_secretKeyIdx == -1) _secretKeyIdx = keys.First().Key;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key")) if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Log in with XIVAuth (experimental)"))
{
_registrationInProgress = true;
_ = Task.Run(async () => {
try
{
var reply = await _registerService.XIVAuth(CancellationToken.None).ConfigureAwait(false);
if (!reply.Success)
{
_logger.LogWarning("Registration failed: {err}", reply.ErrorMessage);
_registrationMessage = reply.ErrorMessage;
if (_registrationMessage.IsNullOrEmpty())
_registrationMessage = "An unknown error occured. Please try again later.";
return;
}
_registrationMessage = "Account registered. Welcome to Snowcloak!";
_secretKey = reply.SecretKey ?? "";
_registrationReply = reply;
_registrationSuccess = true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Registration failed");
_registrationSuccess = false;
_registrationMessage = "An unknown error occured. Please try again later.";
}
finally
{
_registrationInProgress = false;
}
});
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add character with existing key"))
{ {
_serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication() _serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication()
{ {
@@ -372,7 +418,43 @@ public class CompactUi : WindowMediatorSubscriberBase
_ = _apiController.CreateConnections(); _ = _apiController.CreateConnections();
} }
ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess
if (_registrationInProgress)
{
ImGui.TextUnformatted("Waiting for the server...");
}
else if (!_registrationMessage.IsNullOrEmpty())
{
if (!_registrationSuccess)
ImGui.TextColored(ImGuiColors.DalamudYellow, _registrationMessage);
else
ImGui.TextWrapped(_registrationMessage);
}
if (_secretKey.Length > 0 && _secretKey.Length != 64)
{
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", ImGuiColors.DalamudRed);
}
else if (_secretKey.Length == 64)
{
using var saveDisabled = ImRaii.Disabled(_uiSharedService.ApiController.ServerState == ServerState.Connecting || _uiSharedService.ApiController.ServerState == ServerState.Reconnecting);
if (ImGui.Button("Save and Connect"))
{
string keyName;
if (_serverManager.CurrentServer == null) _serverManager.SelectServer(0);
if (_registrationReply != null && _secretKey.Equals(_registrationReply.SecretKey, StringComparison.Ordinal))
keyName = _registrationReply.UID + $" (registered {DateTime.Now:yyyy-MM-dd})";
else
keyName = $"Secret Key added on Setup ({DateTime.Now:yyyy-MM-dd})";
_serverManager.CurrentServer!.SecretKeys.Add(_serverManager.CurrentServer.SecretKeys.Select(k => k.Key).LastOrDefault() + 1, new SecretKey()
{
FriendlyName = keyName,
Key = _secretKey,
});
_serverManager.AddCurrentCharacterToServer(save: false);
_ = Task.Run(() => _uiSharedService.ApiController.CreateConnections());
}
}
_uiSharedService.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key); _uiSharedService.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key);
} }
else else

View File

@@ -167,7 +167,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone. The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.
"""); """);
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files may be automatically deleted.
"""); """);
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
Accounts that are inactive for ninety (90) days will be deleted for privacy reasons. Accounts that are inactive for ninety (90) days will be deleted for privacy reasons.
@@ -263,6 +263,39 @@ public partial class IntroUi : WindowMediatorSubscriberBase
ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0); ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0);
ImGui.Separator(); ImGui.Separator();
ImGui.TextUnformatted("If you have not used Snowcloak before, click below to register a new account."); ImGui.TextUnformatted("If you have not used Snowcloak before, click below to register a new account.");
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Log in with XIVAuth (experimental)"))
{
_registrationInProgress = true;
_ = Task.Run(async () => {
try
{
var reply = await _registerService.XIVAuth(CancellationToken.None).ConfigureAwait(false);
if (!reply.Success)
{
_logger.LogWarning("Registration failed: {err}", reply.ErrorMessage);
_registrationMessage = reply.ErrorMessage;
if (_registrationMessage.IsNullOrEmpty())
_registrationMessage = "An unknown error occured. Please try again later.";
return;
}
_registrationMessage = "Account registered. Welcome to Snowcloak!";
_secretKey = reply.SecretKey ?? "";
_registrationReply = reply;
_registrationSuccess = true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Registration failed");
_registrationSuccess = false;
_registrationMessage = "An unknown error occured. Please try again later.";
}
finally
{
_registrationInProgress = false;
}
});
}
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Snowcloak account")) if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Snowcloak account"))
{ {
_registrationInProgress = true; _registrationInProgress = true;
@@ -298,7 +331,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess
if (_registrationInProgress) if (_registrationInProgress)
{ {
ImGui.TextUnformatted("Sending request..."); ImGui.TextUnformatted("Waiting for the server...");
} }
else if (!_registrationMessage.IsNullOrEmpty()) else if (!_registrationMessage.IsNullOrEmpty())
{ {

View File

@@ -9,6 +9,8 @@ using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
using Dalamud.Utility;
using System.Net;
namespace MareSynchronos.WebAPI; namespace MareSynchronos.WebAPI;
@@ -17,16 +19,18 @@ public sealed class AccountRegistrationService : IDisposable
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly ILogger<AccountRegistrationService> _logger; private readonly ILogger<AccountRegistrationService> _logger;
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly DalamudUtilService _dalamudUtilService;
private string GenerateSecretKey() private string GenerateSecretKey()
{ {
return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64))); return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64)));
} }
public AccountRegistrationService(ILogger<AccountRegistrationService> logger, ServerConfigurationManager serverManager) public AccountRegistrationService(ILogger<AccountRegistrationService> logger, DalamudUtilService dalamudUtilService, ServerConfigurationManager serverManager)
{ {
_logger = logger; _logger = logger;
_serverManager = serverManager; _serverManager = serverManager;
_dalamudUtilService = dalamudUtilService;
_httpClient = new( _httpClient = new(
new HttpClientHandler new HttpClientHandler
{ {
@@ -43,6 +47,72 @@ public sealed class AccountRegistrationService : IDisposable
_httpClient.Dispose(); _httpClient.Dispose();
} }
public async Task<RegisterReplyDto> XIVAuth(CancellationToken token)
{
var secretKey = GenerateSecretKey();
var hashedSecretKey = secretKey.GetHash256();
var playerName = _dalamudUtilService.GetPlayerNameAsync().GetAwaiter().GetResult();
var worldId = (ushort)_dalamudUtilService.GetHomeWorldIdAsync().GetAwaiter().GetResult();
var worldName = _dalamudUtilService.WorldData.Value[(worldId)];
var sessionID = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)).Replace('+', '-').Replace('/', '_').TrimEnd('=');
Uri handshakeUri = new Uri("https://account.snowcloak-sync.com/register");
var handshakePayload = new { session_id = sessionID, hashed_secret = hashedSecretKey, character_name = playerName, home_world = worldName };
var handshakeResponse = await _httpClient.PostAsJsonAsync(handshakeUri, handshakePayload, token).ConfigureAwait(false);
handshakeResponse.EnsureSuccessStatusCode();
var register = await handshakeResponse.Content.ReadFromJsonAsync<RegisterResponse>(cancellationToken: token)
.ConfigureAwait(false);
if (register is null || string.IsNullOrWhiteSpace(register.link_url) ||
string.IsNullOrWhiteSpace(register.poll_url))
{
return new RegisterReplyDto() { Success = false, ErrorMessage = "Malformed registration response." };
}
Util.OpenLink(register.link_url);
const int maxAttempts = 600 / 15; // Try once every 15 seconds for 10 minutes
var pollUri = new Uri(register.poll_url);
PollResponse? lastPoll = null;
for (int i = 0; i < maxAttempts; i++)
{
token.ThrowIfCancellationRequested();
using var resp = await _httpClient.GetAsync(pollUri, token).ConfigureAwait(false);
if (resp.StatusCode == HttpStatusCode.Gone)
{
// Server marked this as having been consumed already OR it got TLL'd out
return new RegisterReplyDto()
{
Success = false, ErrorMessage = "Registration session expired. Please try again."
};
}
if (resp.StatusCode == HttpStatusCode.OK)
{
lastPoll = await resp.Content.ReadFromJsonAsync<PollResponse>(cancellationToken: token)
.ConfigureAwait(false);
if (lastPoll?.status?.Equals("bound", StringComparison.OrdinalIgnoreCase) == true)
{
// yay
return new RegisterReplyDto()
{
Success = true, ErrorMessage = null, UID = lastPoll?.uid, SecretKey = secretKey
};
}
// Pending, keep polling
}
await Task.Delay(TimeSpan.FromSeconds(15), token).ConfigureAwait(false);
}
// Timed out
return new RegisterReplyDto()
{
Success = false,
ErrorMessage =
"Timed out waiting for authorisation. Please try again, and complete the process within 10 minutes."
};
}
public async Task<RegisterReplyDto> RegisterAccount(CancellationToken token) public async Task<RegisterReplyDto> RegisterAccount(CancellationToken token)
{ {
var secretKey = GenerateSecretKey(); var secretKey = GenerateSecretKey();
@@ -67,4 +137,19 @@ public sealed class AccountRegistrationService : IDisposable
SecretKey = secretKey SecretKey = secretKey
}; };
} }
private sealed class RegisterResponse
{
public string link_url { get; set; } = "";
public string poll_url { get; set; } = "";
} }
private sealed class PollResponse
{
public string status { get; set; } = "";
public string? uid { get; set; }
}
}