corp-app/CORP-MAUI/Services/ApiService.cs

770 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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