using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PARR.API.Contracts.V1; using PARR.API.Contracts.V1.Responses; using PARR.API.Contracts.V1.Responses.Base; using PARR.API.Controllers.V1.Base; using PARR.Core.Services.HelpDocument.Interfaces; using PARR.Domain.Common.Roles; using PARR.Domain.Settings; using System.Text.Json.Nodes; namespace PARR.API.Controllers.V1 { /// /// Контроллер по работе со справочной документацией /// public class HelpDocumentController : BaseApiController { private readonly IHelpDocumentService _helpDocumentService; private readonly IMapper _mapper; private readonly HelpDocumentSettings _helpDocumentSettings; private readonly ILogger _logger; public HelpDocumentController( IHelpDocumentService helpDocumentService, IMapper mapper, HelpDocumentSettings helpDocumentSettings, ILogger logger ) { _helpDocumentService = helpDocumentService; _mapper = mapper; _helpDocumentSettings = helpDocumentSettings; _logger = logger; } /// /// Получить документ справки /// /// /// Путь к справке (например: ui/billing/invoice-create/vat-calc) [Authorize(Roles = ParrRoles.Administrator.Role)] [HttpGet(ApiRoutes.HelpDocument.GetDocument)] public async Task GetHelpDocument([FromQuery] string helpId) { if (string.IsNullOrWhiteSpace(helpId)) throw new ArgumentNullException(nameof(helpId), "Значение не может быть пустым"); var documnet = await _helpDocumentService.GetHelpDocumentAsync(helpId); if (documnet == null) return NotFound(); var response = _mapper.Map(documnet); return Ok(new Response(response, true)); } /// /// Вебхук от GitLab для автоматического сброса кэша при пуше изменений /// /// [AllowAnonymous] [HttpPost(ApiRoutes.HelpDocument.GitLabWebhook)] public async Task GitLabWebhook([FromBody] JsonNode payload) { _logger.LogInformation("Получен Webhook запрос от GitLab."); #region Проверка безопасности // GitLab передает токен в заголовке X-Gitlab-Token if (!Request.Headers.TryGetValue("X-Gitlab-Token", out var receivedToken)) { _logger.LogWarning("Запрос вебхука отклонен. Отсутствует заголовок X-Gitlab-Token."); return Unauthorized("Отсутствует авторизационный токен вебхука."); } if (receivedToken.ToString() != _helpDocumentSettings.WebhookToken) { _logger.LogWarning("Запрос вебхука отклонен. Передан невалидный Secret Token."); return Unauthorized("Неверный токен вебхука."); } #endregion _logger.LogDebug("Проверка Secret Token пройдена успешно. Начинается обработка изменений."); try { // Проверяем, что это событие push var objectKind = payload["object_kind"]?.ToString(); if (objectKind != "push") { _logger.LogDebug("Игнорируем вебхук: тип события '{ObjectKind}' не является 'push'.", objectKind); return Ok(new { message = "Ignored non-push event" }); } // Извлекаем массив коммитов var commits = payload["commits"]?.AsArray(); if (commits == null || commits.Count == 0) { _logger.LogDebug("В вебхуке отсутствуют коммиты."); return Ok(new { message = "No commits found" }); } var filesToInvalidate = new HashSet(); // Пробегаемся по коммитам и собираем измененные/добавленные файлы foreach (var commit in commits) { var modifiedFiles = commit?["modified"]?.AsArray(); if (modifiedFiles != null) { foreach (var file in modifiedFiles) { var filePath = file?.ToString(); if (filePath != null && filePath.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) { filesToInvalidate.Add(filePath); } } } var addedFiles = commit?["added"]?.AsArray(); if (addedFiles != null) { foreach (var file in addedFiles) { var filePath = file?.ToString(); if (filePath != null && filePath.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) { filesToInvalidate.Add(filePath); } } } } if (filesToInvalidate.Count == 0) { _logger.LogDebug("Среди измененных файлов в коммитах нет документов .md. Кэш не изменен."); return Ok(new { message = "No .md files modified" }); } _logger.LogInformation("Вебхук зафиксировал изменения в {Count} файлах справок. Начинается сброс кэша...", filesToInvalidate.Count); // Инвалидируем кэш для каждого измененного файла foreach (var file in filesToInvalidate) { // Превращаем путь "ui/billing/vat-calc.md" обратно в helpId "ui/billing/vat-calc" var helpId = file.Substring(0, file.Length - 4); _logger.LogDebug("Сброс кэша по вебхуку для HelpId: {HelpId}", helpId); await _helpDocumentService.ClearCacheAsync(helpId); } _logger.LogInformation("Сброс кэша по вебхуку GitLab успешно завершен."); return Ok(new { clearedFilesCount = filesToInvalidate.Count }); } catch (Exception ex) { _logger.LogError(ex, "Ошибка при обработке Webhook от GitLab"); // Возвращаем 200 или 500 в зависимости от политики. // Обычно возвращают BadRequest/InternalServerError, чтобы в GitLab в истории вебхуков была видна ошибка отладки. return StatusCode(500, "Внутренняя ошибка обработки вебхука"); } } } }