1636 lines
66 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}