1636 lines
66 KiB
C#
1636 lines
66 KiB
C#
using System.Diagnostics;
|
||
using System.Net.Http.Headers;
|
||
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
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;
|
||
private bool _tokensLoaded;
|
||
private readonly SemaphoreSlim _tokenLoadSemaphore = new(1, 1);
|
||
|
||
// Ключи для 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>
|
||
/// Создаёт платформенно-специфичный HttpMessageHandler
|
||
/// </summary>
|
||
private static HttpMessageHandler CreatePlatformHandler()
|
||
{
|
||
// Используем стандартны<D0BD><D18B> HttpClientHandler с отключённой проверкой сертификатов для отладки
|
||
// В продакшене следует убрать ServerCertificateCustomValidationCallback
|
||
return new HttpClientHandler
|
||
{
|
||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить текущий access токен (для WebSocket и других сервисов)
|
||
/// </summary>
|
||
public string? GetAccessToken() => _accessToken;
|
||
|
||
public ApiService(ApiConfig? config = null)
|
||
{
|
||
Trace.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||
Trace.WriteLine("║ [ApiService] INITIALIZATION STARTED");
|
||
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
_config = config ?? new ApiConfig();
|
||
|
||
Trace.WriteLine($"[ApiService] BaseURL: {_config.BaseUrl}");
|
||
Trace.WriteLine($"[ApiService] TokenObtainPath: {_config.TokenObtainPath}");
|
||
Trace.WriteLine($"[ApiService] TokenRefreshPath: {_config.TokenRefreshPath}");
|
||
|
||
// Создаём HttpClient с платформенным хэндлером для корректной работы на Android
|
||
HttpMessageHandler handler = CreatePlatformHandler();
|
||
_httpClient = new HttpClient(handler)
|
||
{
|
||
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");
|
||
|
||
Trace.WriteLine($"[ApiService] HttpClient BaseAddress set to: {_httpClient.BaseAddress}");
|
||
Trace.WriteLine("[ApiService] Initialization complete\n");
|
||
|
||
// НЕ загружаем токены здесь - они будут загружены при первом запросе
|
||
}
|
||
|
||
/// <summary>
|
||
/// Гарантирует, что токены загружены из SecureStorage перед выполнением запроса
|
||
/// </summary>
|
||
private async Task EnsureTokensLoadedAsync()
|
||
{
|
||
if (_tokensLoaded) return;
|
||
|
||
await _tokenLoadSemaphore.WaitAsync();
|
||
try
|
||
{
|
||
if (_tokensLoaded) return;
|
||
|
||
Trace.WriteLine("[ApiService] Loading saved tokens...");
|
||
await LoadTokensAsync();
|
||
_tokensLoaded = true;
|
||
}
|
||
finally
|
||
{
|
||
_tokenLoadSemaphore.Release();
|
||
}
|
||
}
|
||
|
||
#region Debug & Logging
|
||
|
||
/// <summary>
|
||
/// Детальный вывод информации о HTTP запросе и ответе для отладки
|
||
/// </summary>
|
||
private void LogHttpDetails(string operation, HttpRequestMessage? request, HttpResponseMessage? response, string? additionalInfo = null)
|
||
{
|
||
Trace.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||
Trace.WriteLine($"║ [ApiService] {operation}");
|
||
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
if (request != null)
|
||
{
|
||
Trace.WriteLine($"║ REQUEST:");
|
||
Trace.WriteLine($"║ Method: {request.Method}");
|
||
Trace.WriteLine($"║ URI: {request.RequestUri}");
|
||
Trace.WriteLine($"║ BaseAddress: {_httpClient.BaseAddress}");
|
||
Trace.WriteLine($"║ Headers:");
|
||
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
|
||
{
|
||
string value = header.Key == "Authorization" ? "[HIDDEN]" : string.Join(", ", header.Value);
|
||
Trace.WriteLine($"║ {header.Key}: {value}");
|
||
}
|
||
if (request.Content != null)
|
||
{
|
||
Trace.WriteLine($"║ Content-Type: {request.Content.Headers.ContentType}");
|
||
}
|
||
}
|
||
|
||
if (response != null)
|
||
{
|
||
Trace.WriteLine($"║ RESPONSE:");
|
||
Trace.WriteLine($"║ Status: {(int)response.StatusCode} {response.StatusCode}");
|
||
Trace.WriteLine($"║ Content-Type: {response.Content.Headers.ContentType?.MediaType ?? "N/A"}");
|
||
Trace.WriteLine($"║ Content-Length: {response.Content.Headers.ContentLength ?? 0} bytes");
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(additionalInfo))
|
||
{
|
||
Trace.WriteLine($"║ INFO:");
|
||
Trace.WriteLine($"║ {additionalInfo}");
|
||
}
|
||
|
||
Trace.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))
|
||
{
|
||
Trace.WriteLine($"[ApiService] ✅ Access token loaded from storage");
|
||
Trace.WriteLine($"[ApiService] Token (first 30 chars): {_accessToken.Substring(0, Math.Min(30, _accessToken.Length))}...");
|
||
Trace.WriteLine($"[ApiService] Token length: {_accessToken.Length}");
|
||
SetAuthorizationHeader(_accessToken);
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine($"[ApiService] ℹ️ No saved access token found");
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(_refreshToken))
|
||
{
|
||
Trace.WriteLine($"[ApiService] ✅ Refresh token loaded from storage");
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine($"[ApiService] ℹ️ No saved refresh token found");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Error loading tokens: {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;
|
||
|
||
Trace.WriteLine($"[ApiService] 💾 Tokens saved to SecureStorage");
|
||
Trace.WriteLine($"[ApiService] Access token length: {accessToken.Length}");
|
||
Trace.WriteLine($"[ApiService] Refresh token length: {refreshToken.Length}");
|
||
|
||
SetAuthorizationHeader(accessToken);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Error saving tokens: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Установить заголовок авторизации
|
||
/// </summary>
|
||
private void SetAuthorizationHeader(string token)
|
||
{
|
||
_httpClient.DefaultRequestHeaders.Authorization =
|
||
new AuthenticationHeaderValue("Bearer", token);
|
||
Trace.WriteLine($"[ApiService] 🔑 Default Authorization header set: Bearer [token]");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Очистить токены
|
||
/// </summary>
|
||
private void ClearTokens()
|
||
{
|
||
Trace.WriteLine("\n[ApiService] 🧹 Clearing all tokens...");
|
||
|
||
_accessToken = null;
|
||
_refreshToken = null;
|
||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||
|
||
SecureStorage.Remove(ACCESS_TOKEN_KEY);
|
||
SecureStorage.Remove(REFRESH_TOKEN_KEY);
|
||
|
||
Trace.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
|
||
{
|
||
LoginRequest loginData = new() { Email = email, Password = password };
|
||
// Используем относительный путь, так как BaseAddress уже установлен
|
||
string path = _config.TokenObtainPath.TrimStart('/');
|
||
|
||
Trace.WriteLine($"\n[ApiService] >>> Начинаем логин для {email}");
|
||
Trace.WriteLine($"[ApiService] >>> Путь токена: {_config.TokenObtainPath} → {path}");
|
||
Trace.WriteLine($"[ApiService] >>> BaseAddress: {_httpClient.BaseAddress}");
|
||
Trace.WriteLine($"[ApiService] >>> Ожидаемый URL: {_httpClient.BaseAddress}{path}\n");
|
||
|
||
// 🔓 Создаем запрос БЕЗ Authorization header (это запрос для получения токена!)
|
||
HttpRequestMessage request = new(HttpMethod.Post, path)
|
||
{
|
||
Content = JsonContent.Create(loginData)
|
||
};
|
||
|
||
// Убираем Authorization header если он был установлен
|
||
request.Headers.Authorization = null;
|
||
|
||
LogHttpDetails("LOGIN REQUEST", request, null, $"Email: {email}");
|
||
|
||
HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||
|
||
LogHttpDetails("LOGIN RESPONSE", request, response);
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
string errorText = await response.Content.ReadAsStringAsync();
|
||
Trace.WriteLine($"\n[ApiService] ❌ Ошибка логина:");
|
||
Trace.WriteLine($" Status Code: {(int)response.StatusCode} {response.StatusCode}");
|
||
Trace.WriteLine($" Request URL: {response.RequestMessage?.RequestUri}");
|
||
Trace.WriteLine($" Error Body: {errorText}\n");
|
||
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = $"Ошибка авторизации ({response.StatusCode}): {errorText}\nURL: {response.RequestMessage?.RequestUri}"
|
||
};
|
||
}
|
||
|
||
// Проверяем Content-Type
|
||
string contentType = response.Content.Headers.ContentType?.MediaType ?? "";
|
||
if (!contentType.Contains("application/json"))
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Неверный Content-Type: {contentType}");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = "Сервер вернул неверный формат ответа"
|
||
};
|
||
}
|
||
|
||
try
|
||
{
|
||
string responseBody = await response.Content.ReadAsStringAsync();
|
||
Trace.WriteLine($"\n[ApiService] Response Body: {responseBody}\n");
|
||
|
||
TokenResponse? tokenData = JsonSerializer.Deserialize<TokenResponse>(responseBody, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
|
||
if (tokenData == null || tokenData.Access == null || tokenData.Refresh == null)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Токены не получены из ответа");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = "Не получены токены доступа"
|
||
};
|
||
}
|
||
|
||
Trace.WriteLine($"[ApiService] ✅ Токены получены успешно");
|
||
Trace.WriteLine($" Access Token: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
|
||
Trace.WriteLine($" Refresh Token: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...\n");
|
||
|
||
// Сохраняем токены
|
||
await SaveTokensAsync(tokenData.Access, tokenData.Refresh);
|
||
|
||
// Получаем информацию о пользователе
|
||
Trace.WriteLine($"[ApiService] >>> Получаем информацию о пользователе...\n");
|
||
User? user = await GetCurrentUserAsync();
|
||
|
||
if (user != null)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ✅ Пользователь: {user.Username} ({user.Email})\n");
|
||
}
|
||
|
||
return new AuthResponse
|
||
{
|
||
Success = true,
|
||
Token = tokenData.Access,
|
||
User = user,
|
||
Message = "Вход выполнен успешно"
|
||
};
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Ошибка разбора JSON: {ex.Message}");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = $"Ошибка разбора ответа: {ex.Message}"
|
||
};
|
||
}
|
||
}
|
||
catch (HttpRequestException ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ HTTP ошибка: {ex.Message}");
|
||
return new AuthResponse
|
||
{
|
||
Success = false,
|
||
Message = $"Ошибка подключения: {ex.Message}"
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.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))
|
||
{
|
||
Trace.WriteLine("[ApiService] ❌ Cannot refresh: no refresh token available");
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
Trace.WriteLine("\n╔════════════════════════════════════════════════════════════════════════════════════");
|
||
Trace.WriteLine("║ [ApiService] TOKEN REFRESH PROCESS");
|
||
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
// Используем относительный путь, так как BaseAddress уже установлен
|
||
string path = _config.TokenRefreshPath.TrimStart('/');
|
||
RefreshRequest refreshData = new RefreshRequest { Refresh = _refreshToken };
|
||
|
||
Trace.WriteLine($"║ Request Path: {path}");
|
||
Trace.WriteLine($"║ Full URL: {_httpClient.BaseAddress}{path}");
|
||
Trace.WriteLine($"║ Refresh Token: {_refreshToken.Substring(0, Math.Min(20, _refreshToken.Length))}...");
|
||
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||
|
||
// Создаем запрос БЕЗ авторизации (для refresh используется только refresh токен в теле)
|
||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, path)
|
||
{
|
||
Content = JsonContent.Create(refreshData)
|
||
};
|
||
|
||
HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||
|
||
Trace.WriteLine($"\n[ApiService] Refresh response: {(int)response.StatusCode} {response.StatusCode}");
|
||
|
||
if (response.IsSuccessStatusCode)
|
||
{
|
||
string responseText = await response.Content.ReadAsStringAsync();
|
||
Trace.WriteLine($"[ApiService] Response body: {responseText}");
|
||
|
||
TokenResponse? tokenData = JsonSerializer.Deserialize<TokenResponse>(responseText, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
|
||
if (!string.IsNullOrEmpty(tokenData?.Access))
|
||
{
|
||
Trace.WriteLine($"\n[ApiService] ✅ New access token received: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
|
||
|
||
_accessToken = tokenData.Access;
|
||
|
||
try
|
||
{
|
||
await SecureStorage.SetAsync(ACCESS_TOKEN_KEY, tokenData.Access);
|
||
}
|
||
catch (Exception storageEx)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ⚠️ Ошибка сохранения access token в SecureStorage: {storageEx.Message}");
|
||
}
|
||
|
||
SetAuthorizationHeader(tokenData.Access);
|
||
|
||
// Если пришёл новый refresh токен, сохраняем и его
|
||
if (!string.IsNullOrEmpty(tokenData.Refresh))
|
||
{
|
||
Trace.WriteLine($"[ApiService] ✅ New refresh token received: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...");
|
||
_refreshToken = tokenData.Refresh;
|
||
|
||
try
|
||
{
|
||
await SecureStorage.SetAsync(REFRESH_TOKEN_KEY, tokenData.Refresh);
|
||
}
|
||
catch (Exception storageEx)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ⚠️ Ошибка сохранения refresh token в SecureStorage: {storageEx.Message}");
|
||
}
|
||
}
|
||
|
||
Trace.WriteLine("[ApiService] ✅ Tokens saved successfully\n");
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine("[ApiService] ❌ No access token in response");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
string errorText = await response.Content.ReadAsStringAsync();
|
||
Trace.WriteLine($"[ApiService] ❌ Refresh failed: {response.StatusCode}");
|
||
Trace.WriteLine($" Error: {errorText}");
|
||
}
|
||
|
||
// Не удалось обновить токены - очищаем их
|
||
Trace.WriteLine("[ApiService] ⚠️ Clearing all tokens due to refresh failure\n");
|
||
ClearTokens();
|
||
return false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Exception during token refresh: {ex.Message}");
|
||
Trace.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
|
||
{
|
||
// ⚡ Гарантируем, что токены загружены перед запросом
|
||
if (requiresAuth)
|
||
{
|
||
await EnsureTokensLoadedAsync();
|
||
}
|
||
|
||
// Используем относительный путь, так как BaseAddress установлен
|
||
string relativePath = path.TrimStart('/');
|
||
|
||
// Добавляем query параметры
|
||
if (queryParams != null && queryParams.Count != 0)
|
||
{
|
||
string query = string.Join("&", queryParams.Select(kvp =>
|
||
$"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
|
||
relativePath = $"{relativePath}?{query}";
|
||
}
|
||
|
||
HttpRequestMessage request = new HttpRequestMessage(method, relativePath);
|
||
|
||
// 🔑 АВТОМАТИЧЕСКИ добавляем Authorization header если есть токен
|
||
if (requiresAuth && !string.IsNullOrEmpty(_accessToken))
|
||
{
|
||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||
Trace.WriteLine($"[ApiService] 🔑 Authorization header added with token");
|
||
}
|
||
|
||
// Добавляем дополнительные заголовки
|
||
if (headers != null)
|
||
{
|
||
foreach (KeyValuePair<string, string> header in headers)
|
||
{
|
||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||
}
|
||
}
|
||
|
||
// Добавляем JSON body
|
||
if (jsonData != null)
|
||
{
|
||
request.Content = JsonContent.Create(jsonData);
|
||
}
|
||
|
||
Trace.WriteLine($"\n[ApiService] >>> {method} Request");
|
||
Trace.WriteLine($" Path: {path} → {relativePath}");
|
||
Trace.WriteLine($" Full URL: {_httpClient.BaseAddress}{relativePath}");
|
||
Trace.WriteLine($" Requires Auth: {requiresAuth}");
|
||
Trace.WriteLine($" Has Token: {!string.IsNullOrEmpty(_accessToken)}");
|
||
if (jsonData != null)
|
||
{
|
||
Trace.WriteLine($" Has Body: Yes");
|
||
}
|
||
|
||
HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||
|
||
Trace.WriteLine($"[ApiService] <<< Response: {(int)response.StatusCode} {response.StatusCode}");
|
||
Trace.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))
|
||
{
|
||
Trace.WriteLine($"[ApiService] 🔄 Token expired or invalid, attempting refresh...");
|
||
Trace.WriteLine($" Current access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
|
||
Trace.WriteLine($" Refresh token available: Yes\n");
|
||
|
||
if (await RefreshTokensAsync())
|
||
{
|
||
Trace.WriteLine($"[ApiService] ✅ Token successfully refreshed!");
|
||
Trace.WriteLine($" New access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
|
||
Trace.WriteLine($" Retrying original request...\n");
|
||
|
||
// Токен обновлён, повторяем запрос (retry=false чтобы избежать бесконечного цикла)
|
||
return await RequestAsync<T>(method, path, jsonData, queryParams, headers, retry: false, requiresAuth);
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Token refresh failed - tokens cleared");
|
||
Trace.WriteLine($" User needs to login again\n");
|
||
}
|
||
}
|
||
|
||
string responseText = await response.Content.ReadAsStringAsync();
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Request failed:");
|
||
Trace.WriteLine($" Status: {(int)response.StatusCode} {response.StatusCode}");
|
||
Trace.WriteLine($" URL: {response.RequestMessage?.RequestUri}");
|
||
Trace.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
|
||
});
|
||
Trace.WriteLine($"[ApiService] ✅ Data parsed successfully\n");
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
Trace.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)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Request exception: {ex.Message}");
|
||
Trace.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
|
||
|
||
// ============================================
|
||
// EMPLOYEES (Сотрудники)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список сотрудников с пагинацией
|
||
/// </summary>
|
||
public async Task<PaginatedResponse<User>> GetEmployeesPaginatedAsync(int? page = null, int? pageSize = null, string? search = null)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||
if (!string.IsNullOrEmpty(search)) queryParams["search"] = search;
|
||
|
||
ApiResponse<PaginatedResponse<User>> response = await GetAsync<PaginatedResponse<User>>("api/v1/employees/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<User>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения сотрудников: {ex.Message}");
|
||
return new PaginatedResponse<User>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить список всех активных сотрудников (все страницы)
|
||
/// </summary>
|
||
public async Task<List<User>> GetAllEmployeesAsync()
|
||
{
|
||
try
|
||
{
|
||
List<User> allEmployees = new();
|
||
int currentPage = 1;
|
||
PaginatedResponse<User>? response;
|
||
|
||
do
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>
|
||
{
|
||
["active"] = "true",
|
||
["page"] = currentPage.ToString()
|
||
};
|
||
|
||
ApiResponse<PaginatedResponse<User>> apiResponse = await GetAsync<PaginatedResponse<User>>("api/v1/employees/", queryParams);
|
||
response = apiResponse.Ok && apiResponse.Data != null ? apiResponse.Data : new PaginatedResponse<User>();
|
||
|
||
if (response.Results.Any())
|
||
{
|
||
allEmployees.AddRange(response.Results);
|
||
currentPage++;
|
||
}
|
||
} while (!string.IsNullOrEmpty(response.Next));
|
||
|
||
Trace.WriteLine($"[ApiService] ✅ Загружено активных сотрудников: {allEmployees.Count}");
|
||
return allEmployees;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Ошибка получения всех активных сотрудников: {ex.Message}");
|
||
return new List<User>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вернуть полный URL изображения, если передан относительный путь — добавить BaseUrl
|
||
/// </summary>
|
||
public string? GetFullImageUrl(string? url)
|
||
{
|
||
if (string.IsNullOrEmpty(url)) return null;
|
||
|
||
// Если это data-uri — не трактуем как URL
|
||
if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return null;
|
||
|
||
// Protocol-relative
|
||
if (url.StartsWith("//")) return "https:" + url;
|
||
|
||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return url;
|
||
|
||
// Относительный путь — приклеиваем базовый URL
|
||
return _config?.BaseUrl?.TrimEnd('/') + "/" + url.TrimStart('/');
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загрузить аватарки для списка пользователей
|
||
/// </summary>
|
||
public Task LoadUsersAvatarsAsync(List<User>? users)
|
||
{
|
||
if (users == null) return Task.CompletedTask;
|
||
|
||
foreach (var user in users)
|
||
{
|
||
user.AvatarUrl = GetFullImageUrl(user.AvatarUrl);
|
||
}
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить информацию о конкретном пользователе по ID
|
||
/// </summary>
|
||
public async Task<User?> GetUserByIdAsync(int userId)
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<User> response = await GetAsync<User>($"api/v1/employees/{userId}/");
|
||
|
||
if (response.Ok && response.Data != null)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ✅ Пользователь загружен: {response.Data.FullName}");
|
||
return response.Data;
|
||
}
|
||
|
||
Trace.WriteLine($"[ApiService] ❌ Пользователь с ID {userId} не найден");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Ошибка получения пользователя {userId}: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить информацию о текущем пользователе
|
||
/// Использует эндпоинт /api/v1/employees/me/
|
||
/// </summary>
|
||
public async Task<User?> GetCurrentUserAsync()
|
||
{
|
||
try
|
||
{
|
||
Trace.WriteLine("\n╔═══════════════════════════════════════════════════════════════════════════════════");
|
||
Trace.WriteLine("║ [ApiService] FETCHING CURRENT USER PROFILE");
|
||
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||
Trace.WriteLine($"║ Endpoint: api/v1/employees/me/");
|
||
Trace.WriteLine($"║ Has Access Token: {!string.IsNullOrEmpty(_accessToken)}");
|
||
if (!string.IsNullOrEmpty(_accessToken))
|
||
{
|
||
Trace.WriteLine($"║ Token (first 30 chars): {_accessToken.Substring(0, Math.Min(30, _accessToken.Length))}...");
|
||
}
|
||
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════\n");
|
||
|
||
ApiResponse<User> response = await GetAsync<User>("api/v1/employees/me/");
|
||
|
||
if (response.Ok && response.Data != null)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ✅ Current user profile loaded successfully");
|
||
Trace.WriteLine($" User ID: {response.Data.Id}");
|
||
Trace.WriteLine($" Email: {response.Data.Email}");
|
||
Trace.WriteLine($" Name: {response.Data.FirstName} {response.Data.LastName}");
|
||
Trace.WriteLine($" Position: {response.Data.PositionName}");
|
||
Trace.WriteLine($" Departments: {response.Data.Departments?.Count ?? 0}");
|
||
Trace.WriteLine($" Skills: {response.Data.Skills?.Count ?? 0}\n");
|
||
return response.Data;
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Failed to load current user profile");
|
||
Trace.WriteLine($" Status: {response.Status}");
|
||
Trace.WriteLine($" Error: {response.ErrorMessage ?? "Unknown error"}");
|
||
Trace.WriteLine($" Response Text: {response.Text?.Substring(0, Math.Min(200, response.Text?.Length ?? 0))}\n");
|
||
return null;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Exception while getting current user profile: {ex.Message}");
|
||
Trace.WriteLine($" StackTrace: {ex.StackTrace}\n");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить текущего пользователя (alias для GetCurrentUserAsync)
|
||
/// </summary>
|
||
public async Task<User?> GetUserAsync()
|
||
{
|
||
return await GetCurrentUserAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить сотрудника по ID
|
||
/// </summary>
|
||
public async Task<User?> GetEmployeeByIdAsync(int id)
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<User> response = await GetAsync<User>($"api/v1/employees/{id}/");
|
||
return response.Ok ? response.Data : null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения сотрудника {id}: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// DEPARTMENTS (Отделы)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список отделов с пагинацией
|
||
/// </summary>
|
||
public async Task<PaginatedResponse<Department>> GetDepartmentsPaginatedAsync(int? page = null, int? pageSize = null)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||
|
||
ApiResponse<PaginatedResponse<Department>> response = await GetAsync<PaginatedResponse<Department>>("api/v1/departments/", queryParams);
|
||
|
||
// Логируем сырой JSON ответ
|
||
if (response.Ok && !string.IsNullOrEmpty(response.Text))
|
||
{
|
||
Trace.WriteLine($"[ApiService] 📄 Raw JSON response (first 500 chars):");
|
||
Trace.WriteLine(response.Text.Substring(0, Math.Min(500, response.Text.Length)));
|
||
}
|
||
|
||
// Логируем первый отдел для проверки
|
||
if (response.Ok && response.Data != null && response.Data.Results.Any())
|
||
{
|
||
var firstDept = response.Data.Results.First();
|
||
Trace.WriteLine($"[ApiService] 📊 Первый загруженный отдел:");
|
||
Trace.WriteLine($" - ID: {firstDept.Id}");
|
||
Trace.WriteLine($" - Name: '{firstDept.Name}'");
|
||
Trace.WriteLine($" - Description: '{firstDept.Description}'");
|
||
Trace.WriteLine($" - EmployeesCount: {firstDept.EmployeesCount}");
|
||
Trace.WriteLine($" - Head: {firstDept.Head?.FullName ?? "null"}");
|
||
}
|
||
|
||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Department>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения отделов: {ex.Message}");
|
||
return new PaginatedResponse<Department>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить список всех отделов
|
||
/// </summary>
|
||
public async Task<List<Department>> GetAllDepartmentsAsync()
|
||
{
|
||
try
|
||
{
|
||
List<Department> allDepartments = new List<Department>();
|
||
int currentPage = 1;
|
||
PaginatedResponse<Department>? response;
|
||
|
||
do
|
||
{
|
||
response = await GetDepartmentsPaginatedAsync(page: currentPage);
|
||
if (response.Results.Any())
|
||
{
|
||
allDepartments.AddRange(response.Results);
|
||
currentPage++;
|
||
}
|
||
} while (!string.IsNullOrEmpty(response.Next));
|
||
|
||
return allDepartments;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения всех отделов: {ex.Message}");
|
||
return new List<Department>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить информацию о конкретном отделе
|
||
/// </summary>
|
||
public async Task<Department?> GetDepartmentByIdAsync(int departmentId)
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<Department> response = await GetAsync<Department>($"api/v1/departments/{departmentId}/");
|
||
return response.Ok && response.Data != null ? response.Data : null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения отдела {departmentId}: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить список сотрудников отдела
|
||
/// </summary>
|
||
public async Task<List<User>> GetDepartmentEmployeesAsync(int departmentId)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>
|
||
{
|
||
{ "department", departmentId.ToString() }
|
||
};
|
||
|
||
List<User> allEmployees = new List<User>();
|
||
int currentPage = 1;
|
||
|
||
do
|
||
{
|
||
queryParams["page"] = currentPage.ToString();
|
||
var apiResponse = await GetAsync<PaginatedResponse<User>>("api/v1/employees/", queryParams);
|
||
|
||
if (!apiResponse.Ok || apiResponse.Data == null)
|
||
break;
|
||
|
||
var response = apiResponse.Data;
|
||
|
||
if (response.Results.Any())
|
||
{
|
||
allEmployees.AddRange(response.Results);
|
||
currentPage++;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(response.Next))
|
||
break;
|
||
|
||
} while (true);
|
||
|
||
return allEmployees;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения сотрудников отдела {departmentId}: {ex.Message}");
|
||
return new List<User>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// POSITIONS (Должности)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список всех должностей
|
||
/// </summary>
|
||
public async Task<List<Position>> GetPositionsAsync()
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<List<Position>> response = await GetAsync<List<Position>>("api/v1/positions/");
|
||
return response.Ok && response.Data != null ? response.Data : new List<Position>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения должностей: {ex.Message}");
|
||
return new List<Position>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// SKILLS (Навыки)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список всех навыков
|
||
/// </summary>
|
||
public async Task<List<Skill>> GetSkillsAsync()
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<List<Skill>> response = await GetAsync<List<Skill>>("api/v1/skills/");
|
||
return response.Ok && response.Data != null ? response.Data : new List<Skill>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения навыков: {ex.Message}");
|
||
return new List<Skill>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// CALENDAR EVENTS (События календаря)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить события календаря
|
||
/// </summary>
|
||
public async Task<List<CalendarEvent>> GetCalendarEventsAsync()
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<List<CalendarEvent>> response = await GetAsync<List<CalendarEvent>>("api/v1/calendar/events/");
|
||
return response.Ok && response.Data != null ? response.Data : new List<CalendarEvent>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения событий календаря: {ex.Message}");
|
||
return new List<CalendarEvent>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// DOCUMENTS (Документы)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список документов с пагинацией
|
||
/// </summary>
|
||
public async Task<PaginatedResponse<Document>> GetDocumentsPaginatedAsync(int? page = null, int? pageSize = null)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||
|
||
ApiResponse<PaginatedResponse<Document>> response = await GetAsync<PaginatedResponse<Document>>("api/v1/documents/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Document>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения документов: {ex.Message}");
|
||
return new PaginatedResponse<Document>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить все документы (с автоматической пагинацией)
|
||
/// </summary>
|
||
public async Task<List<Document>> GetAllDocumentsAsync()
|
||
{
|
||
List<Document> allDocuments = new List<Document>();
|
||
int page = 1;
|
||
bool hasMore = true;
|
||
|
||
try
|
||
{
|
||
Trace.WriteLine("[ApiService] >>> Загрузка всех документов...");
|
||
|
||
while (hasMore)
|
||
{
|
||
PaginatedResponse<Document> response = await GetDocumentsPaginatedAsync(page);
|
||
|
||
if (response.Results != null && response.Results.Any())
|
||
{
|
||
allDocuments.AddRange(response.Results);
|
||
Trace.WriteLine($"[ApiService] >>> Загружено документов: {allDocuments.Count} / {response.Count}");
|
||
hasMore = !string.IsNullOrEmpty(response.Next);
|
||
page++;
|
||
}
|
||
else
|
||
{
|
||
hasMore = false;
|
||
}
|
||
}
|
||
|
||
Trace.WriteLine($"[ApiService] ✅ Всего загружено документов: {allDocuments.Count}");
|
||
return allDocuments;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Ошибка загрузки документов: {ex.Message}");
|
||
return allDocuments;
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// REQUESTS (Запросы)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список запросов с пагинацией
|
||
/// </summary>
|
||
public async Task<PaginatedResponse<Request>> GetRequestsPaginatedAsync(int? page = null, int? pageSize = null)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||
|
||
ApiResponse<PaginatedResponse<Request>> response = await GetAsync<PaginatedResponse<Request>>("api/v1/requests/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Request>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения запросов: {ex.Message}");
|
||
return new PaginatedResponse<Request>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить все заявления (с автоматической пагинацией)
|
||
/// </summary>
|
||
public async Task<List<Request>> GetAllRequestsAsync()
|
||
{
|
||
List<Request> allRequests = new List<Request>();
|
||
int page = 1;
|
||
bool hasMore = true;
|
||
|
||
try
|
||
{
|
||
Trace.WriteLine("[ApiService] >>> Загрузка всех заявлений...");
|
||
|
||
while (hasMore)
|
||
{
|
||
PaginatedResponse<Request> response = await GetRequestsPaginatedAsync(page);
|
||
|
||
if (response.Results != null && response.Results.Any())
|
||
{
|
||
allRequests.AddRange(response.Results);
|
||
Trace.WriteLine($"[ApiService] >>> Загружено заявлений: {allRequests.Count} / {response.Count}");
|
||
hasMore = !string.IsNullOrEmpty(response.Next);
|
||
page++;
|
||
}
|
||
else
|
||
{
|
||
hasMore = false;
|
||
}
|
||
}
|
||
|
||
Trace.WriteLine($"[ApiService] ✅ Всего загружено заявлений: {allRequests.Count}");
|
||
return allRequests;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Ошибка загрузки заявлений: {ex.Message}");
|
||
return allRequests;
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// EMPLOYEE ACTIONS (Действия сотрудников)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить историю действий сотрудников
|
||
/// </summary>
|
||
public async Task<List<EmployeeAction>> GetEmployeeActionsAsync()
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<List<EmployeeAction>> response = await GetAsync<List<EmployeeAction>>("api/v1/employee-actions/");
|
||
return response.Ok && response.Data != null ? response.Data : new List<EmployeeAction>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения действий сотрудников: {ex.Message}");
|
||
return new List<EmployeeAction>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// POSTS (Посты/Новости)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список постов с пагинацией
|
||
/// </summary>
|
||
public async Task<PaginatedResponse<Post>> GetPostsPaginatedAsync(int? page = null, int? pageSize = null)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||
|
||
ApiResponse<PaginatedResponse<Post>> response = await GetAsync<PaginatedResponse<Post>>("api/v1/posts/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Post>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения постов: {ex.Message}");
|
||
return new PaginatedResponse<Post>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// COMMENTS (Комментарии)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список комментариев с пагинацией
|
||
/// </summary>
|
||
public async Task<PaginatedResponse<Comment>> GetCommentsPaginatedAsync(int? page = null, int? pageSize = null, int? postId = null)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||
if (postId.HasValue) queryParams["post"] = postId.Value.ToString();
|
||
|
||
ApiResponse<PaginatedResponse<Comment>> response = await GetAsync<PaginatedResponse<Comment>>("api/v1/comments/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Comment>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения комментариев: {ex.Message}");
|
||
return new PaginatedResponse<Comment>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// DEPARTMENT ROLES (Роли отделов)
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список ролей в отделах с пагинацией
|
||
/// </summary>
|
||
public async Task<PaginatedResponse<DepartmentRole>> GetDepartmentRolesPaginatedAsync(int? page = null, int? pageSize = null)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new Dictionary<string, string>();
|
||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||
|
||
ApiResponse<PaginatedResponse<DepartmentRole>> response = await GetAsync<PaginatedResponse<DepartmentRole>>("api/v1/department-roles/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<DepartmentRole>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения ролей отделов: {ex.Message}");
|
||
return new PaginatedResponse<DepartmentRole>();
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// LEGACY/COMPATIBILITY METHODS
|
||
// ============================================
|
||
|
||
/// <summary>
|
||
/// Получить список сотрудников (устаревший метод для обратной совместимости)
|
||
/// </summary>
|
||
[Obsolete("Используйте GetAllEmployeesAsync() или GetEmployeesPaginatedAsync()")]
|
||
public Task<List<User>> GetEmployeesAsync() => GetAllEmployeesAsync();
|
||
|
||
/// <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/";
|
||
|
||
ApiResponse<List<Message>> response = await GetAsync<List<Message>>(path);
|
||
return response.Ok && response.Data != null ? response.Data : new List<Message>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка получения сообщений: {ex.Message}");
|
||
return new List<Message>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отправить сообщение через REST API (резервный метод)
|
||
/// </summary>
|
||
public async Task<bool> SendMessageAsync(Message message)
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<Message> response = await PostAsync<Message>("api/v1/messages/", message);
|
||
return response.Ok;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка отправки сообщения: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Поиск пользователей
|
||
/// </summary>
|
||
public async Task<List<User>> SearchUsersAsync(string query)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> queryParams = new() { { "q", query } };
|
||
ApiResponse<List<User>> response = await GetAsync<List<User>>("api/v1/users/search/", queryParams);
|
||
return response.Ok && response.Data != null ? response.Data : new List<User>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка поиска: {ex.Message}");
|
||
return new List<User>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновить статус пользователя
|
||
/// </summary>
|
||
public async Task<bool> UpdateStatusAsync(bool isOnline)
|
||
{
|
||
try
|
||
{
|
||
ApiResponse<object> response = await PutAsync<object>("api/v1/users/status/", new { isOnline });
|
||
return response.Ok;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"Ошибка обновления статуса: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загрузить изображение (в виде байтов) с авторизацией
|
||
/// </summary>
|
||
public async Task<byte[]?> GetImageBytesAsync(string url)
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrEmpty(url)) return null;
|
||
|
||
// Если URL относительный - добавляем базу
|
||
if (!url.StartsWith("http"))
|
||
{
|
||
url = _config.BaseUrl.TrimEnd('/') + "/" + url.TrimStart('/');
|
||
}
|
||
|
||
await EnsureTokensLoadedAsync();
|
||
|
||
// Добавляем токен, если есть
|
||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
|
||
if (!string.IsNullOrEmpty(_accessToken))
|
||
{
|
||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||
}
|
||
|
||
HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||
if (response.IsSuccessStatusCode)
|
||
{
|
||
string? mediaType = response.Content.Headers.ContentType?.MediaType;
|
||
if (mediaType != null && !mediaType.StartsWith("image/"))
|
||
{
|
||
Trace.WriteLine($"[ApiService] ⚠️ URL {url} вернул не изображение, а {mediaType}");
|
||
return null;
|
||
}
|
||
|
||
byte[] bytes = await response.Content.ReadAsByteArrayAsync();
|
||
Trace.WriteLine($"[ApiService] ✅ Загружено изображение {url} ({bytes.Length} bytes)");
|
||
return bytes;
|
||
}
|
||
|
||
Trace.WriteLine($"[ApiService] Ошибка загрузки изображения {url}: {response.StatusCode}");
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] Исключение при загрузке изображения: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Предзагрузить аватары для списка пользователей
|
||
/// </summary>
|
||
public async Task PreloadUserAvatarsAsync(IEnumerable<User> users)
|
||
{
|
||
var userList = users.ToList();
|
||
var tasks = new List<Task>();
|
||
|
||
foreach (var user in userList)
|
||
{
|
||
// Пропускаем, если аватар уже загружен
|
||
if (user.IsAvatarLoaded)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Если Base64 - добавляем в очередь загрузки
|
||
if (user.IsAvatarBase64)
|
||
{
|
||
tasks.Add(LoadUserAvatarAsync(user));
|
||
continue;
|
||
}
|
||
|
||
string? url = user.DisplayAvatarUrl;
|
||
if (string.IsNullOrEmpty(url))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Проверяем файловый кэш
|
||
string? cachedPath = ImageCacheService.Instance.GetCachedFilePath(url);
|
||
if (!string.IsNullOrEmpty(cachedPath) && File.Exists(cachedPath))
|
||
{
|
||
// Устанавливаем путь к файлу
|
||
user.SetCachedAvatarFilePath(cachedPath);
|
||
}
|
||
else
|
||
{
|
||
// Добавляем в очередь загрузки
|
||
tasks.Add(LoadUserAvatarAsync(user));
|
||
}
|
||
}
|
||
|
||
if (tasks.Count > 0)
|
||
{
|
||
Trace.WriteLine($"[ApiService] Загрузка {tasks.Count} аватаров...");
|
||
await Task.WhenAll(tasks);
|
||
Trace.WriteLine($"[ApiService] ✅ Аватары загружены");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загрузить аватар для одного пользователя (файловый кэш)
|
||
/// </summary>
|
||
public async Task LoadUserAvatarAsync(User user)
|
||
{
|
||
try
|
||
{
|
||
// Проверяем, если аватар уже загружен
|
||
if (user.IsAvatarLoaded)
|
||
{
|
||
Trace.WriteLine($"[ApiService] LoadUserAvatarAsync for {user.FullName}: Already loaded");
|
||
return;
|
||
}
|
||
|
||
// Сначала проверяем Base64 формат
|
||
if (user.IsAvatarBase64)
|
||
{
|
||
Trace.WriteLine($"[ApiService] LoadUserAvatarAsync for {user.FullName}: Base64 avatar detected");
|
||
|
||
byte[]? imageBytes = user.GetAvatarBytesFromBase64();
|
||
if (imageBytes != null && imageBytes.Length > 0)
|
||
{
|
||
// Генерируем уникальный путь для файла
|
||
string cacheDir = Path.Combine(FileSystem.CacheDirectory, "avatars");
|
||
if (!Directory.Exists(cacheDir))
|
||
Directory.CreateDirectory(cacheDir);
|
||
|
||
string filePath = Path.Combine(cacheDir, $"avatar_{user.Id}.jpg");
|
||
|
||
await File.WriteAllBytesAsync(filePath, imageBytes);
|
||
user.SetCachedAvatarFilePath(filePath);
|
||
Trace.WriteLine($"[ApiService] ✅ Base64 аватар сохранён: {filePath} ({imageBytes.Length} bytes)");
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine($"[ApiService] ⚠️ Не удалось декодировать Base64 аватар для {user.FullName}");
|
||
}
|
||
}
|
||
|
||
// Далее проверяем URL
|
||
string? url = user.DisplayAvatarUrl;
|
||
Trace.WriteLine($"[ApiService] LoadUserAvatarAsync for {user.FullName}, URL: {url}");
|
||
|
||
if (string.IsNullOrEmpty(url))
|
||
{
|
||
Trace.WriteLine($"[ApiService] ⚠️ URL пустой для {user.FullName}");
|
||
return;
|
||
}
|
||
|
||
// Загружаем через файловый кэш
|
||
string? loadedFilePath = await ImageCacheService.Instance.GetOrLoadImageFileAsync(url, GetImageBytesAsync);
|
||
Trace.WriteLine($"[ApiService] FilePath result: {loadedFilePath}");
|
||
|
||
if (!string.IsNullOrEmpty(loadedFilePath) && File.Exists(loadedFilePath))
|
||
{
|
||
user.SetCachedAvatarFilePath(loadedFilePath);
|
||
Trace.WriteLine($"[ApiService] ✅ Аватар установлен: {loadedFilePath}");
|
||
}
|
||
else
|
||
{
|
||
Trace.WriteLine($"[ApiService] ⚠️ Файл не существует или путь пустой");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Trace.WriteLine($"[ApiService] ❌ Ошибка предзагрузки аватара для {user.FullName}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
|
||
#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; }
|
||
}
|
||
|
||
internal class LoginRequest
|
||
{
|
||
[JsonPropertyName("email")]
|
||
public string Email { get; set; } = string.Empty;
|
||
|
||
[JsonPropertyName("password")]
|
||
public string Password { get; set; } = string.Empty;
|
||
}
|
||
|
||
internal class RefreshRequest
|
||
{
|
||
[JsonPropertyName("refresh")]
|
||
public string Refresh { get; set; } = string.Empty;
|
||
}
|