feat(online): implement IchniOnline API integration with BestHTTP
- Add BestHTTP reference to IchniOnline.asmdef - Create ApiClient.cs: HTTP singleton with JWT auto-injection - Create ApiResponse.cs: ResponseCode enum, GlobalResponse<T>, ApiResult<T> - Create AuthDtos.cs: ThirdPartyLoginRequestDto, LoginRequestDto, RegisterRequestDto, LoginResponseDto, UserResponseDto - Create AuthService.cs: TapTap/password/register/logout flows with events - Extend LoginCacheData.cs: JWT + server user data fields - Extend LoginCacheManager.cs: SaveAuthSession, ClearSession, HasValidSession - Extend ThirdPartyServiceManager.cs: OnLoginWithToken event for AuthService - Update LoginPage.cs: Use AuthService with loading states and null-safe callbacks - Update StartUIPage.cs: Use HasValidSession for session check Fixes post-review: - LoginPage: Add null check for Register success (response may be null) - AuthService: Add try/catch around TapTap login API call
This commit is contained in:
122
Assets/Scripts/Online/Network/ApiClient.cs
Normal file
122
Assets/Scripts/Online/Network/ApiClient.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Best.HTTP;
|
||||
using IchniOnline.Online.Network.Models;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IchniOnline.Online.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// BestHTTP-based API client singleton for IchniOnline backend communication.
|
||||
/// Pure HTTP layer — no business logic.
|
||||
/// </summary>
|
||||
public class IchniOnlineApiClient
|
||||
{
|
||||
private static IchniOnlineApiClient _instance;
|
||||
public static IchniOnlineApiClient Instance => _instance ??= new IchniOnlineApiClient();
|
||||
|
||||
public string BaseUrl { get; set; } = "http://localhost:5433";
|
||||
public string JwtToken { get; set; }
|
||||
|
||||
private IchniOnlineApiClient() { }
|
||||
|
||||
public async Task<ApiResult<T>> GetAsync<T>(string endpoint)
|
||||
{
|
||||
string url = BuildUrl(endpoint);
|
||||
var request = new HTTPRequest(new Uri(url), HTTPMethods.Get);
|
||||
AddAuthHeader(request);
|
||||
|
||||
try
|
||||
{
|
||||
var resp = await request.GetHTTPResponseAsync();
|
||||
return ProcessResponse<T>(resp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Network error", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResult<T>> PostAsync<T>(string endpoint, object body)
|
||||
{
|
||||
string url = BuildUrl(endpoint);
|
||||
var request = new HTTPRequest(new Uri(url), HTTPMethods.Post);
|
||||
request.SetHeader("Content-Type", "application/json");
|
||||
AddAuthHeader(request);
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
string jsonBody = JsonUtility.ToJson(body);
|
||||
request.UploadSettings.UploadStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonBody));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var resp = await request.GetHTTPResponseAsync();
|
||||
return ProcessResponse<T>(resp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Network error", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildUrl(string endpoint)
|
||||
{
|
||||
if (string.IsNullOrEmpty(BaseUrl))
|
||||
throw new InvalidOperationException("BaseUrl is not configured.");
|
||||
|
||||
string baseUrl = BaseUrl.TrimEnd('/');
|
||||
string path = endpoint.StartsWith("/") ? endpoint : $"/{endpoint}";
|
||||
return baseUrl + path;
|
||||
}
|
||||
|
||||
private void AddAuthHeader(HTTPRequest request)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(JwtToken))
|
||||
{
|
||||
request.SetHeader("Authorization", $"Bearer {JwtToken}");
|
||||
}
|
||||
}
|
||||
|
||||
private ApiResult<T> ProcessResponse<T>(HTTPResponse resp)
|
||||
{
|
||||
string json = resp.DataAsText;
|
||||
|
||||
if (resp.StatusCode >= 200 && resp.StatusCode < 300)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Empty response body");
|
||||
}
|
||||
|
||||
var response = JsonUtility.FromJson(json, typeof(GlobalResponse<T>)) as GlobalResponse<T>;
|
||||
if (response == null)
|
||||
{
|
||||
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Failed to parse response JSON");
|
||||
}
|
||||
|
||||
if (response.Code == ResponseCode.Ok)
|
||||
{
|
||||
return ApiResult<T>.Ok(response.Data);
|
||||
}
|
||||
|
||||
return ApiResult<T>.Fail(response.Code, response.Message);
|
||||
}
|
||||
|
||||
// Non-2xx: try to parse server error body
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
var errorResponse = JsonUtility.FromJson(json, typeof(GlobalResponseBase)) as GlobalResponseBase;
|
||||
if (errorResponse != null)
|
||||
{
|
||||
return ApiResult<T>.Fail(errorResponse.Code, errorResponse.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResult<T>.Fail(ResponseCode.InternalServerError, $"HTTP error {resp.StatusCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user