using System;
using System.Text;
using System.Threading;
using Cysharp.Threading.Tasks;
using IchniOnline.Online.Models;
using IchniOnline.Online.Network;
using IchniOnline.Online.Network.Models;
using TapSDK.Login;
using UnityEngine;
namespace IchniOnline.Online.Logic
{
///
/// Authentication orchestration service that coordinates TapTap login,
/// password login, registration, and logout flows.
/// Pure orchestration — no UI state management. Results are delivered via events.
///
public static class IchniOnlineAuthService
{
///
/// 登录成功时触发,参数为服务端返回的登录响应(Register 成功时可能为 null)
///
public static event Action OnLoginSuccess;
///
/// 登录失败时触发,参数为错误信息
///
public static event Action OnLoginFailed;
///
/// 登录被用户取消时触发
///
public static event Action OnLoginCanceled;
///
/// TapTap 未绑定账号时触发,参数为 pendingBindOauthId 和浏览器绑定页面 URL
///
public static event Action OnTapTapUnbound;
///
/// 是否正在进行登录流程,用于防止并发登录请求
///
public static bool IsLoggingIn { get; private set; }
///
/// 当前未绑定的 TapTap oauthId
///
private static string _pendingBindOauthId;
private static CancellationTokenSource _pollingCts;
private const string WebBaseUrl = "https://ichni.hoshino.fan";
#region TapTap Login
///
/// 使用 TapTap 登录的完整流程:
/// TapTap SDK → 第三方登录 API → JWT → 缓存 → 事件
///
public static void LoginWithTapTap()
{
if (IsLoggingIn)
{
Debug.LogWarning("[IchniOnlineAuthService] 已有登录流程正在进行中");
return;
}
IsLoggingIn = true;
Action onSuccess = null;
Action onCanceled = null;
Action onFailed = null;
onSuccess = account =>
{
UnsubscribeTapTapEvents(onSuccess, onCanceled, onFailed);
CompleteTapTapLoginAsync(account).Forget();
};
onCanceled = () =>
{
UnsubscribeTapTapEvents(onSuccess, onCanceled, onFailed);
IsLoggingIn = false;
OnLoginCanceled?.Invoke();
};
onFailed = errorMessage =>
{
UnsubscribeTapTapEvents(onSuccess, onCanceled, onFailed);
IsLoggingIn = false;
OnLoginFailed?.Invoke($"TapTap 登录失败: {errorMessage}");
};
ThirdPartyServiceManager.Instance.OnLoginSuccess += onSuccess;
ThirdPartyServiceManager.Instance.OnLoginCanceled += onCanceled;
ThirdPartyServiceManager.Instance.OnLoginFailed += onFailed;
ThirdPartyServiceManager.Instance.StartTapTapLogin();
}
private static async UniTaskVoid CompleteTapTapLoginAsync(TapTapAccount account)
{
if (account?.accessToken == null)
{
IsLoggingIn = false;
OnLoginFailed?.Invoke("TapTap 登录成功但 accessToken 为空");
return;
}
var dto = new ThirdPartyLoginRequestDto
{
token = account.accessToken.kid,
tokenType = account.accessToken.tokenType,
macKey = account.accessToken.macKey,
macAlgorithm = account.accessToken.macAlgorithm
};
try
{
var result = await IchniOnlineApiClient.Instance.PostAsync("/api/auth/third-party/login", dto);
Debug.Log(JsonUtility.ToJson(result.Data));
IsLoggingIn = false;
if (result.IsSuccess && result.Data != null)
{
if (!string.IsNullOrEmpty(result.Data.pendingBindOauthId))
{
_pendingBindOauthId = result.Data.pendingBindOauthId;
var bindUrl = $"{WebBaseUrl}/bind?method=0&id={_pendingBindOauthId}";
OnTapTapUnbound?.Invoke(_pendingBindOauthId, bindUrl);
OpenBrowserBindPage(bindUrl);
_pollingCts?.Cancel();
_pollingCts = new CancellationTokenSource();
PollBindStatusAsync(_pendingBindOauthId, _pollingCts.Token).Forget();
}
else
{
LoginCacheManager.SaveAuthSession(result.Data.token, result.Data);
IchniOnlineApiClient.Instance.JwtToken = result.Data.token;
OnLoginSuccess?.Invoke(result.Data);
}
}
else if (result.IsSuccess && result.Data == null)
{
OnLoginFailed?.Invoke("TapTap login successful but account not bound");
}
else
{
string errorMessage = $"第三方登录 API 失败: {result.Message}";
if (!string.IsNullOrEmpty(result.ErrorDetail))
errorMessage += $" ({result.ErrorDetail})";
OnLoginFailed?.Invoke(errorMessage);
}
}
catch (Exception ex)
{
IsLoggingIn = false;
Debug.LogError($"[IchniOnlineAuthService] TapTap 登录 API 异常: {ex}");
OnLoginFailed?.Invoke($"TapTap 登录 API 异常: {ex.Message}");
}
}
private static void OpenBrowserBindPage(string url)
{
Application.OpenURL(url);
}
private static async UniTaskVoid PollBindStatusAsync(string oauthId, CancellationToken cancellationToken)
{
var startTime = DateTime.UtcNow;
var timeout = TimeSpan.FromMinutes(5);
var pollInterval = TimeSpan.FromSeconds(2);
while (!cancellationToken.IsCancellationRequested)
{
if (DateTime.UtcNow - startTime > timeout)
{
IsLoggingIn = false;
OnLoginFailed?.Invoke("绑定超时");
return;
}
try
{
var result = await IchniOnlineApiClient.Instance.GetAsync(
$"/api/auth/third-party/bind-status?oauthId={oauthId}",
cancellationToken);
if (result.IsSuccess && result.Data != null)
{
if (result.Data.status == "bound" && !string.IsNullOrEmpty(result.Data.token))
{
var loginResponse = new LoginResponseDto
{
token = result.Data.token,
user = result.Data.user
};
LoginCacheManager.SaveAuthSession(result.Data.token, loginResponse);
IchniOnlineApiClient.Instance.JwtToken = result.Data.token;
IsLoggingIn = false;
OnLoginSuccess?.Invoke(loginResponse);
return;
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"[IchniOnlineAuthService] Polling error: {ex.Message}");
}
await UniTask.Delay(pollInterval,DelayType.Realtime,PlayerLoopTiming.Update,cancellationToken);
}
}
public static void HandleIchniProtocolCallback(string token)
{
if (string.IsNullOrEmpty(token))
{
OnLoginFailed?.Invoke("浏览器回调 token 为空");
return;
}
_pollingCts?.Cancel();
_pollingCts = null;
_pendingBindOauthId = null;
IchniOnlineApiClient.Instance.JwtToken = token;
LoginCacheData cachedData = LoginCacheManager.CachedData;
if (cachedData != null)
{
cachedData.jwtToken = token;
cachedData.hasServerSession = true;
}
OnLoginSuccess?.Invoke(new LoginResponseDto
{
token = token,
user = cachedData != null ? new UserResponseDto
{
userId = cachedData.userId,
username = cachedData.name,
displayName = cachedData.displayName,
avatarUrl = cachedData.avatarUrl,
permission = cachedData.permission
} : null
});
}
private static void UnsubscribeTapTapEvents(
Action onSuccess,
Action onCanceled,
Action onFailed)
{
if (ThirdPartyServiceManager.Instance != null)
{
ThirdPartyServiceManager.Instance.OnLoginSuccess -= onSuccess;
ThirdPartyServiceManager.Instance.OnLoginCanceled -= onCanceled;
ThirdPartyServiceManager.Instance.OnLoginFailed -= onFailed;
}
}
#endregion
#region Password Login
///
/// 使用用户名和密码登录的完整流程:
/// 获取 session-key → XOR 加密密码 → 登录 API → JWT → 缓存 → 事件
///
public static void LoginWithPassword(string username, string password)
{
if (IsLoggingIn)
{
Debug.LogWarning("[IchniOnlineAuthService] 已有登录流程正在进行中");
return;
}
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
OnLoginFailed?.Invoke("用户名或密码不能为空");
return;
}
IsLoggingIn = true;
LoginWithPasswordAsync(username, password).Forget();
}
private static async UniTaskVoid LoginWithPasswordAsync(string username, string password)
{
try
{
// 1. 获取 session-key
var sessionResult = await IchniOnlineApiClient.Instance.GetAsync("/api/auth/session-key");
if (!sessionResult.IsSuccess)
{
IsLoggingIn = false;
string errorMessage = $"获取 session-key 失败: {sessionResult.Message}";
if (!string.IsNullOrEmpty(sessionResult.ErrorDetail))
errorMessage += $" ({sessionResult.ErrorDetail})";
OnLoginFailed?.Invoke(errorMessage);
return;
}
string sessionKey = sessionResult.Data;
// 2. XOR 加密密码
string encryptedPassword = EncryptPassword(password, sessionKey);
// 3. 调用登录 API
var loginDto = new LoginRequestDto
{
username = username,
encryptedPassword = encryptedPassword,
sessionKey = sessionKey
};
var loginResult = await IchniOnlineApiClient.Instance.PostAsync("/api/auth/login", loginDto);
IsLoggingIn = false;
if (loginResult.IsSuccess)
{
LoginCacheManager.SaveAuthSession(loginResult.Data.token, loginResult.Data);
IchniOnlineApiClient.Instance.JwtToken = loginResult.Data.token;
OnLoginSuccess?.Invoke(loginResult.Data);
}
else
{
string errorMessage = $"登录失败: {loginResult.Message}";
if (!string.IsNullOrEmpty(loginResult.ErrorDetail))
errorMessage += $" ({loginResult.ErrorDetail})";
OnLoginFailed?.Invoke(errorMessage);
}
}
catch (Exception ex)
{
IsLoggingIn = false;
Debug.LogError($"[IchniOnlineAuthService] 密码登录异常: {ex}");
OnLoginFailed?.Invoke($"登录异常: {ex.Message}");
}
}
#endregion
#region Register
///
/// 用户注册流程:POST /api/auth/register
/// 注册成功后触发 OnLoginSuccess(null)(注册不返回 JWT)
///
public static void Register(string username, string password, string displayName)
{
if (IsLoggingIn)
{
Debug.LogWarning("[IchniOnlineAuthService] 已有登录流程正在进行中");
return;
}
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
OnLoginFailed?.Invoke("用户名或密码不能为空");
return;
}
IsLoggingIn = true;
RegisterAsync(username, password, displayName).Forget();
}
private static async UniTaskVoid RegisterAsync(string username, string password, string displayName)
{
try
{
var registerDto = new RegisterRequestDto
{
username = username,
password = password,
displayName = displayName
};
var result = await IchniOnlineApiClient.Instance.PostAsync("/api/auth/register", registerDto);
IsLoggingIn = false;
if (result.IsSuccess)
{
OnLoginSuccess?.Invoke(null);
}
else
{
string errorMessage = $"注册失败: {result.Message}";
if (!string.IsNullOrEmpty(result.ErrorDetail))
errorMessage += $" ({result.ErrorDetail})";
OnLoginFailed?.Invoke(errorMessage);
}
}
catch (Exception ex)
{
IsLoggingIn = false;
Debug.LogError($"[IchniOnlineAuthService] 注册异常: {ex}");
OnLoginFailed?.Invoke($"注册异常: {ex.Message}");
}
}
#endregion
#region Logout
///
/// 登出流程:清除本地会话、登出 TapTap、清除 API JWT Token
///
public static void Logout()
{
LoginCacheManager.ClearSession();
ThirdPartyServiceManager.Instance?.Logout();
IchniOnlineApiClient.Instance.JwtToken = null;
Debug.Log("[IchniOnlineAuthService] 已登出");
}
#endregion
#region Encryption
///
/// 使用 XOR 算法加密密码,与服务器 UserService.DecryptPassword 对应。
/// sessionKey 为服务器返回的 Base64 字符串。
///
public static string EncryptPassword(string password, string sessionKey)
{
if (string.IsNullOrEmpty(password))
throw new ArgumentException("密码不能为空", nameof(password));
if (string.IsNullOrEmpty(sessionKey))
throw new ArgumentException("sessionKey 不能为空", nameof(sessionKey));
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] sessionBytes = Convert.FromBase64String(sessionKey);
byte[] encrypted = new byte[passwordBytes.Length];
for (int i = 0; i < passwordBytes.Length; i++)
{
encrypted[i] = (byte)(passwordBytes[i] ^ sessionBytes[i % sessionBytes.Length]);
}
return Convert.ToBase64String(encrypted);
}
#endregion
}
}