162 lines
5.0 KiB
C#
162 lines
5.0 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using Vanguard.Api;
|
|
using Vanguard.Api.Contracts;
|
|
using Vanguard.State;
|
|
|
|
namespace Vanguard.Auth
|
|
{
|
|
/// <summary>
|
|
/// Сессия игрока: login/register, восстановление по refresh-токену,
|
|
/// обновление профиля и выход. Токены не покидают клиент.
|
|
/// </summary>
|
|
public sealed class AuthService
|
|
{
|
|
private readonly ApiClient _api;
|
|
private readonly TokenStore _tokens;
|
|
private readonly PlayerState _player;
|
|
|
|
public AuthService(ApiClient api, TokenStore tokens, PlayerState player)
|
|
{
|
|
_api = api;
|
|
_tokens = tokens;
|
|
_player = player;
|
|
}
|
|
|
|
public bool IsAuthenticated => _tokens.HasRefreshToken;
|
|
|
|
public async Task<AuthResponse> LoginAsync(string userName, string password)
|
|
{
|
|
var response = await _api.PostAsync<AuthResponse>(
|
|
"/api/auth/login",
|
|
new LoginRequest { UserName = userName, Password = password },
|
|
authenticated: false);
|
|
|
|
SaveSession(response);
|
|
await TryRefreshMeAsync();
|
|
return response;
|
|
}
|
|
|
|
public async Task<AuthResponse> RegisterAsync(string userName, string password)
|
|
{
|
|
var response = await _api.PostAsync<AuthResponse>(
|
|
"/api/auth/register",
|
|
new RegisterRequest { UserName = userName, Password = password },
|
|
authenticated: false);
|
|
|
|
SaveSession(response);
|
|
await TryRefreshMeAsync();
|
|
return response;
|
|
}
|
|
|
|
/// <summary>Восстановить сессию при старте. false — нужен логин.</summary>
|
|
public async Task<bool> TryRestoreAsync()
|
|
{
|
|
if (!_tokens.HasRefreshToken || _tokens.RefreshExpiresAt <= DateTimeOffset.UtcNow)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
await RefreshAsync();
|
|
await TryRefreshMeAsync();
|
|
return true;
|
|
}
|
|
catch (ApiException exception) when (exception.StatusCode == 401)
|
|
{
|
|
ClearSession();
|
|
return false;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Сервер временно недоступен: токены не трогаем, покажем логин.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<AuthResponse> RefreshAsync()
|
|
{
|
|
if (!_tokens.HasRefreshToken)
|
|
throw new ApiException(401, "auth.no-refresh", "Нет refresh-токена.");
|
|
|
|
var response = await _api.PostAsync<AuthResponse>(
|
|
"/api/auth/refresh",
|
|
new RefreshRequest { RefreshToken = _tokens.RefreshToken },
|
|
authenticated: false);
|
|
|
|
SaveSession(response);
|
|
return response;
|
|
}
|
|
|
|
public async Task<MeResponse> RefreshMeAsync()
|
|
{
|
|
if (_tokens.IsAccessExpired && _tokens.HasRefreshToken)
|
|
await RefreshAsync();
|
|
|
|
try
|
|
{
|
|
var me = await _api.GetAsync<MeResponse>("/api/auth/me");
|
|
_player.Apply(me);
|
|
return me;
|
|
}
|
|
catch (ApiException exception) when (exception.StatusCode == 401 && _tokens.HasRefreshToken)
|
|
{
|
|
await RefreshAsync();
|
|
var me = await _api.GetAsync<MeResponse>("/api/auth/me");
|
|
_player.Apply(me);
|
|
return me;
|
|
}
|
|
}
|
|
|
|
public async Task LogoutAsync()
|
|
{
|
|
try
|
|
{
|
|
if (_tokens.HasRefreshToken)
|
|
{
|
|
await _api.PostAsync(
|
|
"/api/auth/logout",
|
|
new RefreshRequest { RefreshToken = _tokens.RefreshToken },
|
|
authenticated: false);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Выход не должен зависеть от доступности сервера.
|
|
}
|
|
finally
|
|
{
|
|
ClearSession();
|
|
}
|
|
}
|
|
|
|
private async Task TryRefreshMeAsync()
|
|
{
|
|
try
|
|
{
|
|
await RefreshMeAsync();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Логин уже состоялся; профиль можно обновить на мета-экране.
|
|
}
|
|
}
|
|
|
|
private void SaveSession(AuthResponse response)
|
|
{
|
|
_tokens.Save(
|
|
response.AccessToken,
|
|
response.RefreshToken,
|
|
response.AccessExpiresAt,
|
|
response.RefreshExpiresAt);
|
|
|
|
_player.Apply(response);
|
|
}
|
|
|
|
private void ClearSession()
|
|
{
|
|
_tokens.Clear();
|
|
_player.Clear();
|
|
}
|
|
}
|
|
}
|