feat: proxy para arquivos dos modulos com ratelimit

This commit is contained in:
Ricardo Carneiro 2025-06-08 18:49:54 -03:00
parent 6b79a44e39
commit f3de10cc4f
5 changed files with 576 additions and 0 deletions

View File

@ -0,0 +1,113 @@
using Microsoft.AspNetCore.Mvc;
using OnlyOneAccessTemplate.Services;
namespace OnlyOneAccessTemplate.Controllers
{
[Route("api/modules")]
public class DynamicProxyController : ControllerBase
{
private readonly IModuleService _moduleService;
private readonly HttpClient _httpClient;
private readonly IRateLimitService _rateLimitService;
private readonly ILogger<DynamicProxyController> _logger;
public DynamicProxyController(
IModuleService moduleService,
HttpClient httpClient,
IRateLimitService rateLimitService,
ILogger<DynamicProxyController> logger)
{
_moduleService = moduleService;
_httpClient = httpClient;
_rateLimitService = rateLimitService;
_logger = logger;
}
[HttpPost("{moduleId}/{action}")]
[HttpGet("{moduleId}/{action}")]
[HttpPut("{moduleId}/{action}")]
[HttpDelete("{moduleId}/{action}")]
public async Task<IActionResult> ProxyRequest(string moduleId, string action)
{
try
{
// Buscar configuração do módulo
var module = await _moduleService.GetModuleConfigAsync(moduleId);
if (module == null || !module.IsActive)
{
return NotFound(new { success = false, message = "Módulo não encontrado ou inativo" });
}
// Rate limiting
var clientIP = GetClientIP();
await _rateLimitService.RecordRequestAsync(clientIP);
if (await _rateLimitService.ShouldShowCaptchaAsync(clientIP))
{
return StatusCode(429, new
{
success = false,
message = "Rate limit exceeded",
requiresCaptcha = true
});
}
// Verificar se o endpoint existe no mapeamento
if (!module.ProxyMappings.ContainsKey(action))
{
return NotFound(new { success = false, message = $"Ação '{action}' não disponível para este módulo" });
}
var targetUrl = module.ProxyMappings[action];
// Preparar requisição
var requestMessage = new HttpRequestMessage(
new HttpMethod(Request.Method),
targetUrl + Request.QueryString);
// Copiar headers necessários
foreach (var header in module.Headers)
{
requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
// Copiar corpo da requisição se necessário
if (Request.ContentLength > 0)
{
requestMessage.Content = new StreamContent(Request.Body);
if (Request.ContentType != null)
{
requestMessage.Content.Headers.TryAddWithoutValidation("Content-Type", Request.ContentType);
}
}
_logger.LogInformation("Proxying {Method} request to {ModuleId}.{Action} -> {TargetUrl}",
Request.Method, moduleId, action, targetUrl);
// Fazer requisição
var response = await _httpClient.SendAsync(requestMessage);
var responseContent = await response.Content.ReadAsStringAsync();
// Retornar resposta
Response.StatusCode = (int)response.StatusCode;
if (response.Content.Headers.ContentType?.MediaType != null)
{
Response.ContentType = response.Content.Headers.ContentType.MediaType;
}
return Content(responseContent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro no proxy para {ModuleId}.{Action}", moduleId, action);
return StatusCode(500, new { success = false, message = "Erro interno do servidor" });
}
}
private string GetClientIP()
{
return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Mvc;
using OnlyOneAccessTemplate.Services;
namespace OnlyOneAccessTemplate.Controllers
{
[Route("api/menu")]
public class MenuController : ControllerBase
{
private readonly IModuleService _moduleService;
public MenuController(IModuleService moduleService)
{
_moduleService = moduleService;
}
[HttpGet("converters")]
public async Task<IActionResult> GetConvertersMenu(string language = "pt")
{
try
{
var modules = await _moduleService.GetAllActiveModulesAsync();
var menuItems = modules
.Where(m => m.ShowInMenu && m.IsActive && m.IsHealthy)
.OrderBy(m => m.MenuOrder)
.ThenBy(m => m.MenuTitle)
.GroupBy(m => m.MenuCategory)
.Select(g => new
{
category = g.Key,
items = g.Select(m => new
{
moduleId = m.ModuleId,
title = m.SeoTitles.ContainsKey(language) ? m.SeoTitles[language] : m.MenuTitle,
description = m.SeoDescriptions.ContainsKey(language) ? m.SeoDescriptions[language] : m.MenuDescription,
icon = m.MenuIcon,
url = $"/{language}/{m.RequestBy}",
order = m.MenuOrder,
isNew = m.CreatedAt > DateTime.UtcNow.AddDays(-7) // Novos nos últimos 7 dias
}).ToList()
}).ToList();
return Ok(new { success = true, menu = menuItems });
}
catch (Exception ex)
{
return StatusCode(500, new { success = false, message = ex.Message });
}
}
}
}

View File

@ -0,0 +1,326 @@
using Microsoft.AspNetCore.Mvc;
using OnlyOneAccessTemplate.Services;
using OnlyOneAccessTemplate.Models;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http.Extensions;
namespace OnlyOneAccessTemplate.Controllers
{
[Route("api/module-management")]
[ApiController]
public class ModuleManagementController : ControllerBase
{
private readonly IModuleService _moduleService;
private readonly ILogger<ModuleManagementController> _logger;
private readonly IConfiguration _configuration;
public ModuleManagementController(
IModuleService moduleService,
ILogger<ModuleManagementController> logger,
IConfiguration configuration)
{
_moduleService = moduleService;
_logger = logger;
_configuration = configuration;
}
[HttpPost("register")]
public async Task<IActionResult> RegisterModule([FromBody] ModuleRegistrationRequest request)
{
try
{
_logger.LogInformation("Registrando novo módulo: {ModuleId}", request.ModuleId);
// Verificar se módulo já existe
var existing = await _moduleService.GetModuleConfigAsync(request.ModuleId);
if (existing != null)
{
return BadRequest(new { success = false, message = "Módulo já existe" });
}
// Testar conectividade com o módulo
var healthUrl = $"{request.BaseUrl.TrimEnd('/')}/{request.HealthEndpoint?.TrimStart('/') ?? "api/converter/health"}";
var isHealthy = await TestModuleHealth(healthUrl);
// Gerar API Key
var apiKey = GenerateApiKey();
// Criar configuração
var moduleConfig = new ModuleConfig
{
ModuleId = request.ModuleId,
Name = request.Name,
Url = $"{request.BaseUrl.TrimEnd('/')}/modules/{request.ModuleId}",
RequestBy = request.ModuleId,
IsActive = request.AutoActivate.HasValue && request.AutoActivate.Value && isHealthy,
CacheMinutes = request.CacheMinutes ?? 5,
// Proxy Configuration
UseProxy = true,
ProxyEndpoint = $"/api/modules/{request.ModuleId}",
ProxyMappings = new Dictionary<string, string>
{
["convert"] = $"{request.BaseUrl}/api/converter/convert",
["config"] = $"{request.BaseUrl}/api/converter/config",
["health"] = $"{request.BaseUrl}/api/converter/health"
},
// Security
ApiKey = apiKey,
AllowedOrigins = request.AllowedOrigins ?? new List<string> { Request.GetDisplayUrl() },
RateLimitPerMinute = request.RateLimitPerMinute ?? 60,
// Menu
MenuTitle = request.MenuTitle ?? request.Name,
MenuDescription = request.MenuDescription,
MenuIcon = request.MenuIcon ?? "fas fa-exchange-alt",
MenuCategory = request.MenuCategory ?? "Conversores",
MenuOrder = request.MenuOrder ?? 0,
ShowInMenu = request.ShowInMenu ?? true,
// SEO
SeoTitles = request.SeoTitles ?? new Dictionary<string, string>(),
SeoDescriptions = request.SeoDescriptions ?? new Dictionary<string, string>(),
SeoKeywords = request.SeoKeywords ?? new Dictionary<string, string>(),
// Technical
HealthEndpoint = request.HealthEndpoint ?? "/api/converter/health",
HealthCheckIntervalMinutes = request.HealthCheckIntervalMinutes ?? 5,
AutoStart = request.AutoActivate ?? true,
Version = request.Version ?? "1.0.0",
IsHealthy = isHealthy,
LastHealthCheck = DateTime.UtcNow,
// Developer
DeveloperName = request.DeveloperName,
DeveloperEmail = request.DeveloperEmail,
Repository = request.Repository,
Documentation = request.Documentation,
// Headers
Headers = new Dictionary<string, string>
{
["X-API-Key"] = apiKey,
["User-Agent"] = "ConvertIt-MainApp/1.0"
}
};
await _moduleService.SaveModuleConfigAsync(moduleConfig);
_logger.LogInformation("Módulo {ModuleId} registrado com sucesso", request.ModuleId);
return Ok(new
{
success = true,
message = "Módulo registrado com sucesso",
moduleId = request.ModuleId,
apiKey = apiKey,
proxyEndpoint = moduleConfig.ProxyEndpoint,
isHealthy = isHealthy,
isActive = moduleConfig.IsActive
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao registrar módulo {ModuleId}", request.ModuleId);
return StatusCode(500, new { success = false, message = ex.Message });
}
}
[HttpGet("modules")]
public async Task<IActionResult> ListModules()
{
try
{
var modules = await _moduleService.GetAllActiveModulesAsync();
var result = modules.Select(m => new
{
moduleId = m.ModuleId,
name = m.Name,
isActive = m.IsActive,
isHealthy = m.IsHealthy,
lastHealthCheck = m.LastHealthCheck,
version = m.Version,
menuTitle = m.MenuTitle,
menuCategory = m.MenuCategory,
showInMenu = m.ShowInMenu,
developerName = m.DeveloperName,
proxyEndpoint = m.ProxyEndpoint
});
return Ok(new { success = true, modules = result });
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao listar módulos");
return StatusCode(500, new { success = false, message = ex.Message });
}
}
[HttpPost("modules/{moduleId}/toggle")]
public async Task<IActionResult> ToggleModule(string moduleId)
{
try
{
var module = await _moduleService.GetModuleConfigAsync(moduleId);
if (module == null)
{
return NotFound(new { success = false, message = "Módulo não encontrado" });
}
module.IsActive = !module.IsActive;
module.UpdatedAt = DateTime.UtcNow;
await _moduleService.SaveModuleConfigAsync(module);
_logger.LogInformation("Módulo {ModuleId} {Status}", moduleId,
module.IsActive ? "ativado" : "desativado");
return Ok(new
{
success = true,
moduleId = moduleId,
isActive = module.IsActive,
message = $"Módulo {(module.IsActive ? "ativado" : "desativado")} com sucesso"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao alternar módulo {ModuleId}", moduleId);
return StatusCode(500, new { success = false, message = ex.Message });
}
}
[HttpPost("modules/{moduleId}/health-check")]
public async Task<IActionResult> CheckModuleHealth(string moduleId)
{
try
{
var module = await _moduleService.GetModuleConfigAsync(moduleId);
if (module == null)
{
return NotFound(new { success = false, message = "Módulo não encontrado" });
}
var healthUrl = $"{GetBaseUrl(module.Url)}{module.HealthEndpoint}";
var isHealthy = await TestModuleHealth(healthUrl);
module.IsHealthy = isHealthy;
module.LastHealthCheck = DateTime.UtcNow;
await _moduleService.SaveModuleConfigAsync(module);
return Ok(new
{
success = true,
moduleId = moduleId,
isHealthy = isHealthy,
lastCheck = module.LastHealthCheck,
healthUrl = healthUrl
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao verificar saúde do módulo {ModuleId}", moduleId);
return StatusCode(500, new { success = false, message = ex.Message });
}
}
[HttpDelete("modules/{moduleId}")]
public async Task<IActionResult> UnregisterModule(string moduleId)
{
try
{
var module = await _moduleService.GetModuleConfigAsync(moduleId);
if (module == null)
{
return NotFound(new { success = false, message = "Módulo não encontrado" });
}
// Aqui você implementaria a remoção (dependendo de como está armazenado)
// Por enquanto, vamos apenas desativar
module.IsActive = false;
module.UpdatedAt = DateTime.UtcNow;
await _moduleService.SaveModuleConfigAsync(module);
_logger.LogInformation("Módulo {ModuleId} desregistrado", moduleId);
return Ok(new
{
success = true,
message = "Módulo desregistrado com sucesso"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao desregistrar módulo {ModuleId}", moduleId);
return StatusCode(500, new { success = false, message = ex.Message });
}
}
private async Task<bool> TestModuleHealth(string healthUrl)
{
try
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(10);
var response = await client.GetAsync(healthUrl);
return response.IsSuccessStatusCode;
}
catch
{
return false;
}
}
private string GenerateApiKey()
{
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[32];
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
private string GetBaseUrl(string fullUrl)
{
var uri = new Uri(fullUrl);
return $"{uri.Scheme}://{uri.Host}:{uri.Port}";
}
}
public class ModuleRegistrationRequest
{
public string ModuleId { get; set; } = "";
public string Name { get; set; } = "";
public string BaseUrl { get; set; } = "";
public bool? AutoActivate { get; set; } = true;
public int? CacheMinutes { get; set; } = 5;
public List<string>? AllowedOrigins { get; set; }
public int? RateLimitPerMinute { get; set; } = 60;
// Menu
public string? MenuTitle { get; set; }
public string? MenuDescription { get; set; }
public string? MenuIcon { get; set; }
public string? MenuCategory { get; set; }
public int? MenuOrder { get; set; }
public bool? ShowInMenu { get; set; } = true;
// SEO
public Dictionary<string, string>? SeoTitles { get; set; }
public Dictionary<string, string>? SeoDescriptions { get; set; }
public Dictionary<string, string>? SeoKeywords { get; set; }
// Technical
public string? HealthEndpoint { get; set; }
public int? HealthCheckIntervalMinutes { get; set; }
public string? Version { get; set; }
// Developer
public string? DeveloperName { get; set; }
public string? DeveloperEmail { get; set; }
public string? Repository { get; set; }
public string? Documentation { get; set; }
}
}

View File

@ -25,5 +25,44 @@ namespace OnlyOneAccessTemplate.Models
public string? JavaScriptFunction { get; set; } // Função de inicialização public string? JavaScriptFunction { get; set; } // Função de inicialização
public string? CssUrl { get; set; } // CSS opcional public string? CssUrl { get; set; } // CSS opcional
public Dictionary<string, string> Assets { get; set; } = new(); // Assets adicionais public Dictionary<string, string> Assets { get; set; } = new(); // Assets adicionais
// ADICIONAR na classe ModuleConfig existente:
// Configurações de Proxy Dinâmico
public string? ProxyEndpoint { get; set; } // "/api/modules/{moduleId}"
public Dictionary<string, string> ProxyMappings { get; set; } = new(); // endpoint -> moduleUrl
public bool UseProxy { get; set; } = true;
// Configurações de Segurança
public string? ApiKey { get; set; } // Gerado automaticamente
public List<string> AllowedOrigins { get; set; } = new();
public int RateLimitPerMinute { get; set; } = 60;
// Configurações de Menu
public string? MenuTitle { get; set; }
public string? MenuDescription { get; set; }
public string? MenuIcon { get; set; }
public string? MenuCategory { get; set; } = "Conversores";
public int MenuOrder { get; set; } = 0;
public bool ShowInMenu { get; set; } = true;
// Configurações de SEO
public Dictionary<string, string> SeoTitles { get; set; } = new();
public Dictionary<string, string> SeoDescriptions { get; set; } = new();
public Dictionary<string, string> SeoKeywords { get; set; } = new();
// Configurações Técnicas
public string? HealthEndpoint { get; set; } = "/api/converter/health";
public int HealthCheckIntervalMinutes { get; set; } = 5;
public bool AutoStart { get; set; } = true;
public string? Version { get; set; }
public DateTime? LastHealthCheck { get; set; }
public bool IsHealthy { get; set; } = false;
// Metadados do Desenvolvedor
public string? DeveloperName { get; set; }
public string? DeveloperEmail { get; set; }
public string? Repository { get; set; }
public string? Documentation { get; set; }
} }
} }

View File

@ -0,0 +1,47 @@
### Registrar o módulo sentence-converter
POST https://localhost:7001/api/module-management/register
Content-Type: application/json
{
"moduleId": "sentence-converter",
"name": "Conversor de Primeira Maiúscula",
"baseUrl": "https://localhost:7002",
"autoActivate": true,
"cacheMinutes": 5,
"rateLimitPerMinute": 60,
"menuTitle": "Primeira Maiúscula",
"menuDescription": "Converte texto para formato de primeira letra maiúscula",
"menuIcon": "fas fa-text-height",
"menuCategory": "Texto",
"menuOrder": 1,
"showInMenu": true,
"seoTitles": {
"pt": "Conversor para Primeira Maiúscula Online - Gratuito",
"en": "Sentence Case Converter Online - Free",
"es": "Convertidor a Mayúscula Inicial en Línea - Gratis"
},
"seoDescriptions": {
"pt": "Converta seu texto para o formato de primeira letra maiúscula rapidamente. Ferramenta gratuita e fácil de usar.",
"en": "Convert your text to sentence case format quickly. Free and easy to use tool.",
"es": "Convierte tu texto al formato de primera letra mayúscula rápidamente. Herramienta gratuita y fácil de usar."
},
"version": "1.0.0",
"developerName": "Seu Nome",
"developerEmail": "seu@email.com",
"repository": "https://github.com/user/sentence-converter"
}
### Listar todos os módulos
GET https://localhost:7001/api/module-management/modules
### Verificar saúde de um módulo
POST https://localhost:7001/api/module-management/modules/sentence-converter/health-check
### Ativar/desativar módulo
POST https://localhost:7001/api/module-management/modules/sentence-converter/toggle
### Buscar menu de conversores
GET https://localhost:7001/api/menu/converters?language=pt