770 lines
30 KiB
C#
770 lines
30 KiB
C#
using System.Diagnostics;
|
||
using System.Net.Http.Headers;
|
||
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
using CORP_MAUI.Models;
|
||
|
||
namespace CORP_MAUI.Services;
|
||
|
||
/// <summary>
|
||
/// Конфигурация API клиента
|
||
/// </summary>
|
||
public class ApiConfig
|
||
{
|
||
public string BaseUrl { get; set; } = "https://corp.robotail.pro";
|
||
public string TokenObtainPath { get; set; } = "/api/auth/token/";
|
||
public string TokenRefreshPath { get; set; } = "/api/auth/token/refresh/";
|
||
public int Timeout { get; set; } = 30;
|
||
public Dictionary<string, string>? DefaultHeaders { get; init; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обёртка для HTTP ответов
|
||
/// </summary>
|
||
public class ApiResponse<T>
|
||
{
|
||
public bool Ok { get; set; }
|
||
public int Status { get; set; }
|
||
public T? Data { get; set; }
|
||
public string? Text { get; set; }
|
||
public string? ErrorMessage { get; set; }
|
||
}
|
||
|
||
public class ApiService : IDisposable
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ApiConfig _config;
|
||
|
||
private string? _accessToken;
|
||
private string? _refreshToken;
|
||
|
||
// Ключи для SecureStorage
|
||
private const string ACCESS_TOKEN_KEY = "api.jwt_access";
|
||
private const string REFRESH_TOKEN_KEY = "api.jwt_refresh";
|
||
|
||
public bool IsAuthenticated => !string.IsNullOrEmpty(_accessToken);
|
||
|
||
/// <summary>
|
||
/// Получить текущий access токен (для WebSocket и других сервисов)
|
||
/// </summary>
|
||
public string? GetAccessToken() => _accessToken;
|
||
|
||
public ApiService(ApiConfig? config = null)
|
||
{
|
||
Debug.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||
Debug.WriteLine("║ [ApiService] INITIALIZATION STARTED");
|
||
Debug.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
_config = config ?? new ApiConfig();
|
||
|
||
Debug.WriteLine($"[ApiService] BaseURL: {_config.BaseUrl}");
|
||
Debug.WriteLine($"[ApiService] TokenObtainPath: {_config.TokenObtainPath}");
|
||
Debug.WriteLine($"[ApiService] TokenRefreshPath: {_config.TokenRefreshPath}");
|
||
|
||
_httpClient = new HttpClient
|
||
{
|
||
BaseAddress = new Uri(_config.BaseUrl),
|
||
Timeout = TimeSpan.FromSeconds(_config.Timeout)
|
||
};
|
||
|
||
// Устанавливаем дефолтные заголовки
|
||
if (_config.DefaultHeaders != null)
|
||
{
|
||
foreach (var header in _config.DefaultHeaders)
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
|
||
}
|
||
}
|
||
|
||
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||
|
||
Debug.WriteLine($"[ApiService] HttpClient BaseAddress set to: {_httpClient.BaseAddress}");
|
||
Debug.WriteLine("[ApiService] Loading saved tokens...");
|
||
|
||
// Загружаем сохраненные токены
|
||
_ = LoadTokensAsync();
|
||
|
||
Debug.WriteLine("[ApiService] Initialization complete\n");
|
||
}
|
||
|
||
#region Debug & Logging
|
||
|
||
/// <summary>
|
||
/// Детальный вывод информации о HTTP запросе и ответе для отладки
|
||
/// </summary>
|
||
private void LogHttpDetails(string operation, HttpRequestMessage? request, HttpResponseMessage? response, string? additionalInfo = null)
|
||
{
|
||
Debug.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||
Debug.WriteLine($"║ [ApiService] {operation}");
|
||
Debug.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
if (request != null)
|
||
{
|
||
Debug.WriteLine($"║ REQUEST:");
|
||
Debug.WriteLine($"║ Method: {request.Method}");
|
||
Debug.WriteLine($"║ URI: {request.RequestUri}");
|
||
Debug.WriteLine($"║ BaseAddress: {_httpClient.BaseAddress}");
|
||
Debug.WriteLine($"║ Headers:");
|
||
foreach (var header in request.Headers)
|
||
{
|
||
var value = header.Key == "Authorization" ? "[HIDDEN]" : string.Join(", ", header.Value);
|
||
Debug.WriteLine($"║ {header.Key}: {value}");
|
||
}
|
||
if (request.Content != null)
|
||
{
|
||
Debug.WriteLine($"║ Content-Type: {request.Content.Headers.ContentType}");
|
||
}
|
||
}
|
||
|
||
if (response != null)
|
||
{
|
||
Debug.WriteLine($"║ RESPONSE:");
|
||
Debug.WriteLine($"║ Status: {(int)response.StatusCode} {response.StatusCode}");
|
||
Debug.WriteLine($"║ Content-Type: {response.Content.Headers.ContentType?.MediaType ?? "N/A"}");
|
||
Debug.WriteLine($"║ Content-Length: {response.Content.Headers.ContentLength ?? 0} bytes");
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(additionalInfo))
|
||
{
|
||
Debug.WriteLine($"║ INFO:");
|
||
Debug.WriteLine($"║ {additionalInfo}");
|
||
}
|
||
|
||
Debug.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Token Management
|
||
|
||
/// <summary>
|
||
/// Загрузить токены из SecureStorage
|
||
/// </summary>
|
||
private async Task LoadTokensAsync()
|
||
{
|
||
try
|
||
{
|
||
_accessToken = await SecureStorage.GetAsync(ACCESS_TOKEN_KEY);
|
||
_refreshToken = await SecureStorage.GetAsync(REFRESH_TOKEN_KEY);
|
||
|
||
if (!string.IsNullOrEmpty(_accessToken))
|
||
{
|
||
SetAuthorizationHeader(_accessToken);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка загрузки токенов: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сохранить токены в SecureStorage
|
||
/// </summary>
|
||
private async Task SaveTokensAsync(string accessToken, string refreshToken)
|
||
{
|
||
try
|
||
{
|
||
await SecureStorage.SetAsync(ACCESS_TOKEN_KEY, accessToken);
|
||
await SecureStorage.SetAsync(REFRESH_TOKEN_KEY, refreshToken);
|
||
|
||
_accessToken = accessToken;
|
||
_refreshToken = refreshToken;
|
||
|
||
SetAuthorizationHeader(accessToken);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка сохранения токенов: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Установить заголовок авторизации
|
||
/// </summary>
|
||
private void SetAuthorizationHeader(string token)
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Authorization =
|
||
new AuthenticationHeaderValue("Bearer", token);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Очистить токены
|
||
/// </summary>
|
||
private void ClearTokens()
|
||
{
|
||
Debug.WriteLine("\n[ApiService] 🧹 Clearing all tokens...");
|
||
|
||
_accessToken = null;
|
||
_refreshToken = null;
|
||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||
|
||
SecureStorage.Remove(ACCESS_TOKEN_KEY);
|
||
SecureStorage.Remove(REFRESH_TOKEN_KEY);
|
||
|
||
Debug.WriteLine("[ApiService] ✅ Tokens cleared, user is logged out\n");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Authentication
|
||
|
||
/// <summary>
|
||
/// Вход в систему с получением JWT токенов
|
||
/// </summary>
|
||
public async Task<AuthResponse> LoginAsync(string email, string password)
|
||
{
|
||
try
|
||
{
|
||
var loginData = new { email, password };
|
||
// Используем относительный путь, так как BaseAddress уже установлен
|
||
var path = _config.TokenObtainPath.TrimStart('/');
|
||
|
||
Debug.WriteLine($"\n[ApiService] >>> Начинаем логин для {email}");
|
||
Debug.WriteLine($"[ApiService] >>> Путь токена: {_config.TokenObtainPath} → {path}");
|
||
Debug.WriteLine($"[ApiService] >>> BaseAddress: {_httpClient.BaseAddress}");
|
||
Debug.WriteLine($"[ApiService] >>> Ожидаемый URL: {_httpClient.BaseAddress}{path}\n");
|
||
|
||
// 🔓 Создаем запрос БЕЗ Authorization header (это запрос для получения токена!)
|
||
var request = new HttpRequestMessage(HttpMethod.Post, path)
|
||
{
|
||
Content = JsonContent.Create(loginData)
|
||
};
|
||
|
||
// Убираем Authorization header если он был установлен
|
||
request.Headers.Authorization = null;
|
||
|
||
LogHttpDetails("LOGIN REQUEST", request, null, $"Email: {email}");
|
||
|
||
var response = await _httpClient.SendAsync(request);
|
||
|
||
LogHttpDetails("LOGIN RESPONSE", request, response);
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
var errorText = await response.Content.ReadAsStringAsync();
|
||
Debug.WriteLine($"\n[ApiService] ❌ Ошибка логина:");
|
||
Debug.WriteLine($" Status Code: {(int)response.StatusCode} {response.StatusCode}");
|
||
Debug.WriteLine($" Request URL: {response.RequestMessage?.RequestUri}");
|
||
Debug.WriteLine($" Error Body: {errorText}\n");
|
||
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = $"Ошибка авторизации ({response.StatusCode}): {errorText}\nURL: {response.RequestMessage?.RequestUri}"
|
||
};
|
||
}
|
||
|
||
// Проверяем Content-Type
|
||
var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
|
||
if (!contentType.Contains("application/json"))
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Неверный Content-Type: {contentType}");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = "Сервер вернул неверный формат ответа"
|
||
};
|
||
}
|
||
|
||
try
|
||
{
|
||
var responseBody = await response.Content.ReadAsStringAsync();
|
||
Debug.WriteLine($"\n[ApiService] Response Body: {responseBody}\n");
|
||
|
||
var tokenData = JsonSerializer.Deserialize<TokenResponse>(responseBody, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
|
||
if (tokenData == null || tokenData.Access == null || tokenData.Refresh == null)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Токены не получены из ответа");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = "Не получены токены доступа"
|
||
};
|
||
}
|
||
|
||
Debug.WriteLine($"[ApiService] ✅ Токены получены успешно");
|
||
Debug.WriteLine($" Access Token: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
|
||
Debug.WriteLine($" Refresh Token: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...\n");
|
||
|
||
// Сохраняем токены
|
||
await SaveTokensAsync(tokenData.Access, tokenData.Refresh);
|
||
|
||
// Получаем информацию о пользователе
|
||
Debug.WriteLine($"[ApiService] >>> Получаем информацию о пользователе...\n");
|
||
var user = await GetCurrentUserAsync();
|
||
|
||
if (user != null)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ✅ Пользователь: {user.Username} ({user.Email})\n");
|
||
}
|
||
|
||
return new AuthResponse
|
||
{
|
||
Success = true,
|
||
Token = tokenData.Access,
|
||
User = user,
|
||
Message = "Вход выполнен успешно"
|
||
};
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Ошибка разбора JSON: {ex.Message}");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = $"Ошибка разбора ответа: {ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
catch (HttpRequestException ex)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ HTTP ошибка: {ex.Message}");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = $"Ошибка подключения: {ex.Message}"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Неожиданная ошибка: {ex.Message}\n{ex.StackTrace}");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = $"Неожиданная ошибка: {ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Выход из системы
|
||
/// </summary>
|
||
public void Logout()
|
||
{
|
||
ClearTokens();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновить access токен используя refresh токен
|
||
/// </summary>
|
||
private async Task<bool> RefreshTokensAsync()
|
||
{
|
||
if (string.IsNullOrEmpty(_refreshToken))
|
||
{
|
||
Debug.WriteLine("[ApiService] ❌ Cannot refresh: no refresh token available");
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
Debug.WriteLine("\n╔════════════════════════════════════════════════════════════════════════════════════");
|
||
Debug.WriteLine("║ [ApiService] TOKEN REFRESH PROCESS");
|
||
Debug.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
// Используем относительный путь, так как BaseAddress уже установлен
|
||
var path = _config.TokenRefreshPath.TrimStart('/');
|
||
var refreshData = new { refresh = _refreshToken };
|
||
|
||
Debug.WriteLine($"║ Request Path: {path}");
|
||
Debug.WriteLine($"║ Full URL: {_httpClient.BaseAddress}{path}");
|
||
Debug.WriteLine($"║ Refresh Token: {_refreshToken.Substring(0, Math.Min(20, _refreshToken.Length))}...");
|
||
Debug.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
// Создаем запрос БЕЗ авторизации (для refresh используется только refresh токен в теле)
|
||
var request = new HttpRequestMessage(HttpMethod.Post, path)
|
||
{
|
||
Content = JsonContent.Create(refreshData)
|
||
};
|
||
|
||
var response = await _httpClient.SendAsync(request);
|
||
|
||
Debug.WriteLine($"\n[ApiService] Refresh response: {(int)response.StatusCode} {response.StatusCode}");
|
||
|
||
if (response.IsSuccessStatusCode)
|
||
{
|
||
var responseText = await response.Content.ReadAsStringAsync();
|
||
Debug.WriteLine($"[ApiService] Response body: {responseText}");
|
||
|
||
var tokenData = JsonSerializer.Deserialize<TokenResponse>(responseText, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
|
||
if (!string.IsNullOrEmpty(tokenData?.Access))
|
||
{
|
||
Debug.WriteLine($"\n[ApiService] ✅ New access token received: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
|
||
|
||
_accessToken = tokenData.Access;
|
||
await SecureStorage.SetAsync(ACCESS_TOKEN_KEY, tokenData.Access);
|
||
SetAuthorizationHeader(tokenData.Access);
|
||
|
||
// Если пришёл новый refresh токен, сохраняем и его
|
||
if (!string.IsNullOrEmpty(tokenData.Refresh))
|
||
{
|
||
Debug.WriteLine($"[ApiService] ✅ New refresh token received: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...");
|
||
_refreshToken = tokenData.Refresh;
|
||
await SecureStorage.SetAsync(REFRESH_TOKEN_KEY, tokenData.Refresh);
|
||
}
|
||
|
||
Debug.WriteLine("[ApiService] ✅ Tokens saved successfully\n");
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
Debug.WriteLine("[ApiService] ❌ No access token in response");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var errorText = await response.Content.ReadAsStringAsync();
|
||
Debug.WriteLine($"[ApiService] ❌ Refresh failed: {response.StatusCode}");
|
||
Debug.WriteLine($" Error: {errorText}");
|
||
}
|
||
|
||
// Не удалось обновить токены - очищаем их
|
||
Debug.WriteLine("[ApiService] ⚠️ Clearing all tokens due to refresh failure\n");
|
||
ClearTokens();
|
||
return false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Exception during token refresh: {ex.Message}");
|
||
Debug.WriteLine($" StackTrace: {ex.StackTrace}\n");
|
||
ClearTokens();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region HTTP Methods
|
||
|
||
|
||
/// <summary>
|
||
/// Базовый метод для HTTP запросов с автоматическим refresh при 401/403
|
||
/// Автоматически добавляет Authorization header если есть токен
|
||
/// </summary>
|
||
private async Task<ApiResponse<T>> RequestAsync<T>(
|
||
HttpMethod method,
|
||
string path,
|
||
object? jsonData = null,
|
||
Dictionary<string, string>? queryParams = null,
|
||
Dictionary<string, string>? headers = null,
|
||
bool retry = true,
|
||
bool requiresAuth = true)
|
||
{
|
||
try
|
||
{
|
||
// Используем относительный путь, так как BaseAddress установлен
|
||
var relativePath = path.TrimStart('/');
|
||
|
||
// Добавляем query параметры
|
||
if (queryParams != null && queryParams.Any())
|
||
{
|
||
var query = string.Join("&", queryParams.Select(kvp =>
|
||
$"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
|
||
relativePath = $"{relativePath}?{query}";
|
||
}
|
||
|
||
var request = new HttpRequestMessage(method, relativePath);
|
||
|
||
// 🔑 АВТОМАТИЧЕСКИ добавляем Authorization header если есть токен
|
||
if (requiresAuth && !string.IsNullOrEmpty(_accessToken))
|
||
{
|
||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||
Debug.WriteLine($"[ApiService] 🔑 Authorization header added with token");
|
||
}
|
||
|
||
// Добавляем дополнительные заголовки
|
||
if (headers != null)
|
||
{
|
||
foreach (var header in headers)
|
||
{
|
||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||
}
|
||
}
|
||
|
||
// Добавляем JSON body
|
||
if (jsonData != null)
|
||
{
|
||
request.Content = JsonContent.Create(jsonData);
|
||
}
|
||
|
||
Debug.WriteLine($"\n[ApiService] >>> {method} Request");
|
||
Debug.WriteLine($" Path: {path} → {relativePath}");
|
||
Debug.WriteLine($" Full URL: {_httpClient.BaseAddress}{relativePath}");
|
||
Debug.WriteLine($" Requires Auth: {requiresAuth}");
|
||
Debug.WriteLine($" Has Token: {!string.IsNullOrEmpty(_accessToken)}");
|
||
if (jsonData != null)
|
||
{
|
||
Debug.WriteLine($" Has Body: Yes");
|
||
}
|
||
|
||
var response = await _httpClient.SendAsync(request);
|
||
|
||
Debug.WriteLine($"[ApiService] <<< Response: {(int)response.StatusCode} {response.StatusCode}");
|
||
Debug.WriteLine($" Content-Type: {response.Content.Headers.ContentType?.MediaType ?? "N/A"}\n");
|
||
|
||
// 🔄 Проверяем на 401/403 и пробуем обновить токен
|
||
if ((response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
|
||
response.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||
&& retry && !string.IsNullOrEmpty(_refreshToken))
|
||
{
|
||
Debug.WriteLine($"[ApiService] 🔄 Token expired or invalid, attempting refresh...");
|
||
Debug.WriteLine($" Current access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
|
||
Debug.WriteLine($" Refresh token available: Yes\n");
|
||
|
||
if (await RefreshTokensAsync())
|
||
{
|
||
Debug.WriteLine($"[ApiService] ✅ Token successfully refreshed!");
|
||
Debug.WriteLine($" New access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
|
||
Debug.WriteLine($" Retrying original request...\n");
|
||
|
||
// Токен обновлён, повторяем запрос (retry=false чтобы избежать бесконечного цикла)
|
||
return await RequestAsync<T>(method, path, jsonData, queryParams, headers, retry: false, requiresAuth);
|
||
}
|
||
else
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Token refresh failed - tokens cleared");
|
||
Debug.WriteLine($" User needs to login again\n");
|
||
}
|
||
}
|
||
|
||
var responseText = await response.Content.ReadAsStringAsync();
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Request failed:");
|
||
Debug.WriteLine($" Status: {(int)response.StatusCode} {response.StatusCode}");
|
||
Debug.WriteLine($" URL: {response.RequestMessage?.RequestUri}");
|
||
Debug.WriteLine($" Body: {responseText.Substring(0, Math.Min(200, responseText.Length))}\n");
|
||
|
||
return new ApiResponse<T>
|
||
{
|
||
Ok = false,
|
||
Status = (int)response.StatusCode,
|
||
Text = responseText,
|
||
ErrorMessage = $"HTTP {response.StatusCode}: {responseText}"
|
||
};
|
||
}
|
||
|
||
// Пытаемся распарсить JSON
|
||
T? data = default;
|
||
if (!string.IsNullOrWhiteSpace(responseText))
|
||
{
|
||
try
|
||
{
|
||
data = JsonSerializer.Deserialize<T>(responseText, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
Debug.WriteLine($"[ApiService] ✅ Data parsed successfully\n");
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ⚠️ JSON parse warning: {ex.Message}\n");
|
||
// Не JSON ответ, игнорируем
|
||
}
|
||
}
|
||
|
||
return new ApiResponse<T>
|
||
{
|
||
Ok = true,
|
||
Status = (int)response.StatusCode,
|
||
Data = data,
|
||
Text = responseText
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"[ApiService] ❌ Request exception: {ex.Message}");
|
||
Debug.WriteLine($" StackTrace: {ex.StackTrace}\n");
|
||
return new ApiResponse<T>
|
||
{
|
||
Ok = false,
|
||
Status = 0,
|
||
ErrorMessage = $"Ошибка запроса: {ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// GET запрос
|
||
/// </summary>
|
||
public Task<ApiResponse<T>> GetAsync<T>(string path, Dictionary<string, string>? queryParams = null)
|
||
{
|
||
return RequestAsync<T>(HttpMethod.Get, path, queryParams: queryParams);
|
||
}
|
||
|
||
/// <summary>
|
||
/// POST запрос
|
||
/// </summary>
|
||
public Task<ApiResponse<T>> PostAsync<T>(string path, object? data = null)
|
||
{
|
||
return RequestAsync<T>(HttpMethod.Post, path, jsonData: data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// PUT запрос
|
||
/// </summary>
|
||
public Task<ApiResponse<T>> PutAsync<T>(string path, object? data = null)
|
||
{
|
||
return RequestAsync<T>(HttpMethod.Put, path, jsonData: data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// PATCH запрос
|
||
/// </summary>
|
||
public Task<ApiResponse<T>> PatchAsync<T>(string path, object? data = null)
|
||
{
|
||
return RequestAsync<T>(HttpMethod.Patch, path, jsonData: data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// DELETE запрос
|
||
/// </summary>
|
||
public Task<ApiResponse<T>> DeleteAsync<T>(string path)
|
||
{
|
||
return RequestAsync<T>(HttpMethod.Delete, path);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region API Methods
|
||
|
||
|
||
/// <summary>
|
||
/// Получить список сотрудников
|
||
/// </summary>
|
||
public async Task<List<User>> GetEmployeesAsync()
|
||
{
|
||
try
|
||
{
|
||
var response = await GetAsync<List<User>>("api/v1/employees/");
|
||
return response.Ok && response.Data != null ? response.Data : new List<User>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка получения пользователей: {ex.Message}");
|
||
return new List<User>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить информацию о текущем пользователе
|
||
/// </summary>
|
||
public async Task<User?> GetCurrentUserAsync()
|
||
{
|
||
try
|
||
{
|
||
var response = await GetAsync<User>("api/v1/employees/me/");
|
||
return response.Ok ? response.Data : null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка получения профиля: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить историю сообщений
|
||
/// </summary>
|
||
public async Task<List<Message>> GetMessagesAsync(int? userId = null)
|
||
{
|
||
try
|
||
{
|
||
string path = userId.HasValue
|
||
? $"api/v1/messages/?user_id={userId.Value}"
|
||
: "api/v1/messages/";
|
||
|
||
var response = await GetAsync<List<Message>>(path);
|
||
return response.Ok && response.Data != null ? response.Data : new List<Message>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка получения сообщений: {ex.Message}");
|
||
return new List<Message>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отправить сообщение через REST API (резервный метод)
|
||
/// </summary>
|
||
public async Task<bool> SendMessageAsync(Message message)
|
||
{
|
||
try
|
||
{
|
||
var response = await PostAsync<Message>("api/v1/messages/", message);
|
||
return response.Ok;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка отправки сообщения: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Поиск пользователей
|
||
/// </summary>
|
||
public async Task<List<User>> SearchUsersAsync(string query)
|
||
{
|
||
try
|
||
{
|
||
var queryParams = new Dictionary<string, string> { { "q", query } };
|
||
var response = await GetAsync<List<User>>("api/v1/users/search/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new List<User>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка поиска: {ex.Message}");
|
||
return new List<User>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновить статус пользователя
|
||
/// </summary>
|
||
public async Task<bool> UpdateStatusAsync(bool isOnline)
|
||
{
|
||
try
|
||
{
|
||
var response = await PutAsync<object>("api/v1/users/status/", new { isOnline });
|
||
return response.Ok;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"Ошибка обновления статуса: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region IDisposable
|
||
|
||
public void Dispose()
|
||
{
|
||
_httpClient.Dispose();
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>
|
||
/// Модель для получения JWT токенов
|
||
/// </summary>
|
||
internal class TokenResponse
|
||
{
|
||
public string? Access { get; set; }
|
||
public string? Refresh { get; set; }
|
||
}
|
||
|