Compare commits
2 Commits
0387df1994
...
55ad73b505
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55ad73b505 | ||
|
|
ffac8a787b |
@ -25,7 +25,8 @@
|
||||
"Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)",
|
||||
"Bash(sed:*)",
|
||||
"Bash(./clean-build.sh:*)",
|
||||
"Bash(git add:*)"
|
||||
"Bash(git add:*)",
|
||||
"Bash(scp:*)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": false
|
||||
|
||||
452
categorias.json
Normal file
452
categorias.json
Normal file
@ -0,0 +1,452 @@
|
||||
[
|
||||
{
|
||||
"name": "Artesanato",
|
||||
"slug": "artesanato",
|
||||
"icon": "🎨",
|
||||
"seoKeywords": [
|
||||
"artesanato",
|
||||
"artesão",
|
||||
"feito à mão",
|
||||
"personalizado",
|
||||
"criativo",
|
||||
"decoração"
|
||||
],
|
||||
"description": "Artesãos e criadores de produtos feitos à mão, decoração e arte personalizada",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Papelaria",
|
||||
"slug": "papelaria",
|
||||
"icon": "📝",
|
||||
"seoKeywords": [
|
||||
"papelaria",
|
||||
"escritório",
|
||||
"material escolar",
|
||||
"impressão",
|
||||
"convites",
|
||||
"personalização"
|
||||
],
|
||||
"description": "Lojas de papelaria, material de escritório, impressão e produtos personalizados",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Coaching",
|
||||
"slug": "coaching",
|
||||
"icon": "🎯",
|
||||
"seoKeywords": [
|
||||
"coaching",
|
||||
"mentoria",
|
||||
"desenvolvimento pessoal",
|
||||
"life coach",
|
||||
"business coach",
|
||||
"liderança"
|
||||
],
|
||||
"description": "Coaches, mentores e profissionais de desenvolvimento pessoal e empresarial",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Fitness",
|
||||
"slug": "fitness",
|
||||
"icon": "💪",
|
||||
"seoKeywords": [
|
||||
"fitness",
|
||||
"academia",
|
||||
"personal trainer",
|
||||
"musculação",
|
||||
"treinamento",
|
||||
"exercícios"
|
||||
],
|
||||
"description": "Personal trainers, academias, estúdios de pilates e profissionais fitness",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Psicologia",
|
||||
"slug": "psicologia",
|
||||
"icon": "🧠",
|
||||
"seoKeywords": [
|
||||
"psicólogo",
|
||||
"terapia",
|
||||
"psicologia",
|
||||
"saúde mental",
|
||||
"consultório",
|
||||
"atendimento"
|
||||
],
|
||||
"description": "Psicólogos, terapeutas e profissionais de saúde mental",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Nutrição",
|
||||
"slug": "nutricao",
|
||||
"icon": "🥗",
|
||||
"seoKeywords": [
|
||||
"nutricionista",
|
||||
"dieta",
|
||||
"nutrição",
|
||||
"alimentação saudável",
|
||||
"consultoria nutricional",
|
||||
"emagrecimento"
|
||||
],
|
||||
"description": "Nutricionistas, consultores em alimentação e profissionais da nutrição",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Moda e Vestuário",
|
||||
"slug": "moda",
|
||||
"icon": "👗",
|
||||
"seoKeywords": [
|
||||
"moda",
|
||||
"vestuário",
|
||||
"roupas",
|
||||
"fashion",
|
||||
"estilista",
|
||||
"costureira"
|
||||
],
|
||||
"description": "Lojas de roupas, estilistas, costureiras e profissionais da moda",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Fotografia",
|
||||
"slug": "fotografia",
|
||||
"icon": "📸",
|
||||
"seoKeywords": [
|
||||
"fotógrafo",
|
||||
"fotografia",
|
||||
"ensaio",
|
||||
"casamento",
|
||||
"eventos",
|
||||
"retratos"
|
||||
],
|
||||
"description": "Fotógrafos profissionais, estúdios fotográficos e serviços de fotografia",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Marketing Digital",
|
||||
"slug": "marketing-digital",
|
||||
"icon": "📱",
|
||||
"seoKeywords": [
|
||||
"marketing digital",
|
||||
"social media",
|
||||
"publicidade",
|
||||
"SEO",
|
||||
"gestão de redes",
|
||||
"digital"
|
||||
],
|
||||
"description": "Agências de marketing digital, gestores de redes sociais e consultores digitais",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Contabilidade",
|
||||
"slug": "contabilidade",
|
||||
"icon": "📊",
|
||||
"seoKeywords": [
|
||||
"contador",
|
||||
"contabilidade",
|
||||
"fiscal",
|
||||
"imposto de renda",
|
||||
"MEI",
|
||||
"consultoria contábil"
|
||||
],
|
||||
"description": "Contadores, escritórios contábeis e consultoria fiscal",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Design",
|
||||
"slug": "design",
|
||||
"icon": "🎨",
|
||||
"seoKeywords": [
|
||||
"designer",
|
||||
"design gráfico",
|
||||
"identidade visual",
|
||||
"logo",
|
||||
"criativo",
|
||||
"branding"
|
||||
],
|
||||
"description": "Designers gráficos, criativos e profissionais de identidade visual",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Consultoria",
|
||||
"slug": "consultoria",
|
||||
"icon": "🤝",
|
||||
"seoKeywords": [
|
||||
"consultor",
|
||||
"consultoria",
|
||||
"assessoria",
|
||||
"especialista",
|
||||
"negócios",
|
||||
"estratégia"
|
||||
],
|
||||
"description": "Consultores especializados, assessoria empresarial e serviços de consultoria",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Pets",
|
||||
"slug": "pets",
|
||||
"icon": "🐕",
|
||||
"seoKeywords": [
|
||||
"veterinário",
|
||||
"pet shop",
|
||||
"animais",
|
||||
"cuidados",
|
||||
"petshop",
|
||||
"adestramento"
|
||||
],
|
||||
"description": "Veterinários, pet shops, adestradores e serviços para animais",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Casa e Jardim",
|
||||
"slug": "casa-jardim",
|
||||
"icon": "🏡",
|
||||
"seoKeywords": [
|
||||
"paisagismo",
|
||||
"jardinagem",
|
||||
"decoração",
|
||||
"casa",
|
||||
"jardim",
|
||||
"plantas"
|
||||
],
|
||||
"description": "Paisagistas, jardineiros, decoradores e serviços para casa e jardim",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Automóveis",
|
||||
"slug": "automoveis",
|
||||
"icon": "🚗",
|
||||
"seoKeywords": [
|
||||
"mecânico",
|
||||
"automóveis",
|
||||
"carros",
|
||||
"oficina",
|
||||
"manutenção",
|
||||
"peças"
|
||||
],
|
||||
"description": "Mecânicos, oficinas, lojas de peças e serviços automotivos",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Turismo",
|
||||
"slug": "turismo",
|
||||
"icon": "✈️",
|
||||
"seoKeywords": [
|
||||
"turismo",
|
||||
"viagem",
|
||||
"agência",
|
||||
"guia turístico",
|
||||
"passeios",
|
||||
"hospedagem"
|
||||
],
|
||||
"description": "Agências de turismo, guias, pousadas e prestadores de serviços turísticos",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Música",
|
||||
"slug": "musica",
|
||||
"icon": "🎵",
|
||||
"seoKeywords": [
|
||||
"músico",
|
||||
"professor de música",
|
||||
"instrumentos",
|
||||
"aulas",
|
||||
"banda",
|
||||
"eventos musicais"
|
||||
],
|
||||
"description": "Músicos, professores de música, bandas e profissionais do entretenimento",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Idiomas",
|
||||
"slug": "idiomas",
|
||||
"icon": "🗣️",
|
||||
"seoKeywords": [
|
||||
"professor de idiomas",
|
||||
"inglês",
|
||||
"espanhol",
|
||||
"tradutor",
|
||||
"aulas particulares",
|
||||
"curso de idiomas"
|
||||
],
|
||||
"description": "Professores de idiomas, tradutores e escolas de línguas",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Limpeza",
|
||||
"slug": "limpeza",
|
||||
"icon": "🧽",
|
||||
"seoKeywords": [
|
||||
"limpeza",
|
||||
"faxina",
|
||||
"diarista",
|
||||
"higienização",
|
||||
"empresa de limpeza",
|
||||
"doméstica"
|
||||
],
|
||||
"description": "Empresas de limpeza, diaristas e serviços de higienização",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Segurança",
|
||||
"slug": "seguranca",
|
||||
"icon": "🛡️",
|
||||
"seoKeywords": [
|
||||
"segurança",
|
||||
"vigilante",
|
||||
"porteiro",
|
||||
"alarmes",
|
||||
"monitoramento",
|
||||
"proteção"
|
||||
],
|
||||
"description": "Empresas de segurança, vigilantes e serviços de proteção",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Eventos",
|
||||
"slug": "eventos",
|
||||
"icon": "🎉",
|
||||
"seoKeywords": [
|
||||
"eventos",
|
||||
"festa",
|
||||
"casamento",
|
||||
"buffet",
|
||||
"decoração de festas",
|
||||
"cerimonial"
|
||||
],
|
||||
"description": "Organizadores de eventos, buffets, decoração e cerimonial",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Transporte",
|
||||
"slug": "transporte",
|
||||
"icon": "🚐",
|
||||
"seoKeywords": [
|
||||
"transporte",
|
||||
"frete",
|
||||
"mudança",
|
||||
"delivery",
|
||||
"motorista",
|
||||
"logística"
|
||||
],
|
||||
"description": "Empresas de transporte, fretes, mudanças e serviços de entrega",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Construção",
|
||||
"slug": "construcao",
|
||||
"icon": "🔨",
|
||||
"seoKeywords": [
|
||||
"construção",
|
||||
"pedreiro",
|
||||
"pintor",
|
||||
"eletricista",
|
||||
"encanador",
|
||||
"reforma"
|
||||
],
|
||||
"description": "Profissionais da construção civil, reformas e manutenção predial",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Joias e Acessórios",
|
||||
"slug": "joias",
|
||||
"icon": "💎",
|
||||
"seoKeywords": [
|
||||
"joias",
|
||||
"bijuterias",
|
||||
"acessórios",
|
||||
"ourives",
|
||||
"relógios",
|
||||
"semijoias"
|
||||
],
|
||||
"description": "Joalherias, bijuterias, ourives e lojas de acessórios",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Odontologia",
|
||||
"slug": "odontologia",
|
||||
"icon": "🦷",
|
||||
"seoKeywords": [
|
||||
"dentista",
|
||||
"odontologia",
|
||||
"clínica dentária",
|
||||
"ortodontia",
|
||||
"implante",
|
||||
"oral"
|
||||
],
|
||||
"description": "Dentistas, clínicas odontológicas e profissionais da área bucal",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Fisioterapia",
|
||||
"slug": "fisioterapia",
|
||||
"icon": "🏥",
|
||||
"seoKeywords": [
|
||||
"fisioterapeuta",
|
||||
"fisioterapia",
|
||||
"reabilitação",
|
||||
"RPG",
|
||||
"massagem",
|
||||
"terapia"
|
||||
],
|
||||
"description": "Fisioterapeutas, clínicas de reabilitação e terapias corporais",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Livraria",
|
||||
"slug": "livraria",
|
||||
"icon": "📚",
|
||||
"seoKeywords": [
|
||||
"livraria",
|
||||
"livros",
|
||||
"sebo",
|
||||
"literatura",
|
||||
"editora",
|
||||
"publicação"
|
||||
],
|
||||
"description": "Livrarias, sebos, editoras e comércio de livros e publicações",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Floricultura",
|
||||
"slug": "floricultura",
|
||||
"icon": "🌸",
|
||||
"seoKeywords": [
|
||||
"floricultura",
|
||||
"flores",
|
||||
"buquê",
|
||||
"plantas",
|
||||
"arranjos",
|
||||
"casamento"
|
||||
],
|
||||
"description": "Floriculturas, arranjos florais e comércio de plantas ornamentais",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Farmácia",
|
||||
"slug": "farmacia",
|
||||
"icon": "💊",
|
||||
"seoKeywords": [
|
||||
"farmácia",
|
||||
"farmacêutico",
|
||||
"medicamentos",
|
||||
"drogaria",
|
||||
"manipulação",
|
||||
"remédios"
|
||||
],
|
||||
"description": "Farmácias, drogarias e farmacêuticos especializados",
|
||||
"isActive": true
|
||||
},
|
||||
{
|
||||
"name": "Delivery",
|
||||
"slug": "delivery",
|
||||
"icon": "🛵",
|
||||
"seoKeywords": [
|
||||
"delivery",
|
||||
"entrega",
|
||||
"motoboy",
|
||||
"comida",
|
||||
"aplicativo",
|
||||
"rápido"
|
||||
],
|
||||
"description": "Serviços de delivery, entregadores e aplicativos de entrega",
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
@ -186,6 +186,8 @@ public class AdminController : Controller
|
||||
|
||||
[HttpPost]
|
||||
[Route("ManagePage")]
|
||||
[RequestSizeLimit(5 * 1024 * 1024)] // Allow 5MB uploads
|
||||
[RequestFormLimits(MultipartBodyLengthLimit = 5 * 1024 * 1024)]
|
||||
public async Task<IActionResult> ManagePage(ManagePageViewModel model)
|
||||
{
|
||||
ViewBag.IsHomePage = false;
|
||||
@ -231,10 +233,27 @@ public class AdminController : Controller
|
||||
{
|
||||
_logger.LogError(ex, "Error uploading profile image");
|
||||
ModelState.AddModelError("ProfileImageFile", "Erro ao fazer upload da imagem. Tente novamente.");
|
||||
TempData["ImageError"] = "Erro ao processar a imagem. Verifique o formato e tamanho.";
|
||||
|
||||
// Preservar dados do form e repopular dropdowns
|
||||
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;
|
||||
var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString());
|
||||
|
||||
// Repopulate dropdowns
|
||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
||||
model.MaxLinksAllowed = userPlanType.GetMaxLinksPerPage();
|
||||
model.AllowProductLinks = planLimitations.AllowProductLinks;
|
||||
|
||||
// Preservar ProfileImageId existente se estava editando
|
||||
if (!model.IsNewPage && !string.IsNullOrEmpty(model.Id))
|
||||
{
|
||||
var existingPage = await _userPageService.GetPageByIdAsync(model.Id);
|
||||
if (existingPage != null)
|
||||
{
|
||||
model.ProfileImageId = existingPage.ProfileImageId;
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
@ -328,10 +347,20 @@ public class AdminController : Controller
|
||||
return RedirectToAction("Dashboard");
|
||||
}
|
||||
|
||||
// IMPORTANTE: Preservar ProfileImageId da página existente se não houver novo upload
|
||||
// IMPORTANTE: Tratar remoção de imagem ou preservar existente se não houver novo upload
|
||||
if (model.ProfileImageFile == null || model.ProfileImageFile.Length == 0)
|
||||
{
|
||||
model.ProfileImageId = existingPage.ProfileImageId;
|
||||
if (model.ProfileImageId == "REMOVE_IMAGE")
|
||||
{
|
||||
// Usuário quer remover a imagem existente
|
||||
model.ProfileImageId = null;
|
||||
_logger.LogInformation("Profile image removed by user request");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Preservar imagem existente
|
||||
model.ProfileImageId = existingPage.ProfileImageId;
|
||||
}
|
||||
}
|
||||
|
||||
await UpdateUserPageFromModel(existingPage, model);
|
||||
|
||||
@ -23,6 +23,14 @@ namespace BCards.Web.Middleware
|
||||
{
|
||||
await _next(context);
|
||||
|
||||
// Verificar se a resposta já começou antes de modificar headers
|
||||
if (context.Response.HasStarted)
|
||||
{
|
||||
_logger.LogDebug("AuthCache: Response already started, skipping header modifications for {Path}",
|
||||
context.Request.Path.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Aplicar headers apenas para páginas HTML (não APIs, imagens, etc)
|
||||
if (context.Response.ContentType?.StartsWith("text/html") == true)
|
||||
{
|
||||
|
||||
@ -4,12 +4,10 @@ using BCards.Web.Repositories;
|
||||
using BCards.Web.HealthChecks;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using System.Globalization;
|
||||
using Stripe;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||
using SendGrid;
|
||||
using BCards.Web.Middleware;
|
||||
@ -22,11 +20,9 @@ using Serilog.Sinks.OpenSearch;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Configure Serilog with environment-specific settings
|
||||
var isDevelopment = builder.Environment.IsDevelopment();
|
||||
var hostname = Environment.MachineName;
|
||||
|
||||
// 🔥 CORREÇÃO 1: Habilitar SelfLog ANTES de configurar o logger
|
||||
Serilog.Debugging.SelfLog.Enable(msg =>
|
||||
{
|
||||
Console.WriteLine($"[SERILOG SELF] {DateTime.Now:HH:mm:ss} {msg}");
|
||||
@ -112,7 +108,7 @@ else
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.StaticFiles", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||
// 🔥 GARANTIR CONSOLE EM PRODUÇÃO TAMBÉM
|
||||
|
||||
.WriteTo.Console(
|
||||
restrictedToMinimumLevel: LogEventLevel.Information,
|
||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
@ -161,7 +157,6 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 REMOVER OS Task.Delay E Log.CloseAndFlush desnecessários
|
||||
var logger = loggerConfig.CreateLogger();
|
||||
Log.Logger = logger;
|
||||
|
||||
@ -176,7 +171,6 @@ builder.Host.UseSerilog();
|
||||
// Log startup information
|
||||
Log.Information("Starting BCards application on {Hostname} in {Environment} mode", hostname, builder.Environment.EnvironmentName);
|
||||
|
||||
// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
@ -186,7 +180,6 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
options.ForwardLimit = null;
|
||||
});
|
||||
|
||||
// 🔥 OTIMIZAÇÃO: Sistema de Compressão de Response (Brotli + Gzip)
|
||||
builder.Services.AddResponseCompression(options =>
|
||||
{
|
||||
options.EnableForHttps = true;
|
||||
@ -410,12 +403,25 @@ builder.Services.AddScoped<IEmailService, EmailService>();
|
||||
|
||||
builder.Services.AddScoped<IImageStorageService, GridFSImageStorage>();
|
||||
|
||||
// Configure upload limits for file handling (images up to 5MB)
|
||||
builder.Services.Configure<FormOptions>(options =>
|
||||
{
|
||||
options.MultipartBodyLengthLimit = 10 * 1024 * 1024;
|
||||
options.MultipartBodyLengthLimit = 5 * 1024 * 1024; // 5MB
|
||||
options.ValueLengthLimit = int.MaxValue;
|
||||
options.ValueCountLimit = int.MaxValue;
|
||||
options.KeyLengthLimit = int.MaxValue;
|
||||
options.BufferBody = true;
|
||||
options.BufferBodyLengthLimit = 5 * 1024 * 1024; // 5MB
|
||||
options.MultipartBodyLengthLimit = 5 * 1024 * 1024; // 5MB
|
||||
options.MultipartHeadersLengthLimit = 16384;
|
||||
});
|
||||
|
||||
// Configure Kestrel server limits for larger requests
|
||||
builder.Services.Configure<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions>(options =>
|
||||
{
|
||||
options.Limits.MaxRequestBodySize = 5 * 1024 * 1024; // 5MB
|
||||
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2);
|
||||
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<ILivePageRepository, LivePageRepository>();
|
||||
@ -555,11 +561,11 @@ app.UseRequestLocalization();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseMiddleware<BCards.Web.Middleware.SmartCacheMiddleware>();
|
||||
app.UseMiddleware<BCards.Web.Middleware.AuthCacheMiddleware>();
|
||||
app.UseMiddleware<BCards.Web.Middleware.PlanLimitationMiddleware>();
|
||||
app.UseMiddleware<BCards.Web.Middleware.PageStatusMiddleware>();
|
||||
app.UseMiddleware<BCards.Web.Middleware.PreviewTokenMiddleware>();
|
||||
app.UseMiddleware<SmartCacheMiddleware>();
|
||||
app.UseMiddleware<AuthCacheMiddleware>();
|
||||
app.UseMiddleware<PlanLimitationMiddleware>();
|
||||
app.UseMiddleware<PageStatusMiddleware>();
|
||||
app.UseMiddleware<PreviewTokenMiddleware>();
|
||||
app.UseMiddleware<ModerationAuthMiddleware>();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
@ -640,7 +646,6 @@ app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
|
||||
// Initialize default data
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var themeService = scope.ServiceProvider.GetRequiredService<IThemeService>();
|
||||
@ -660,12 +665,10 @@ using (var scope = app.Services.CreateScope())
|
||||
await categoryService.InitializeDefaultCategoriesAsync();
|
||||
}
|
||||
|
||||
// 🔥 CORREÇÃO 12: Logs adicionais após inicialização
|
||||
Log.Information("Default themes and categories initialized successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
||||
Log.Error(ex, "Error initializing default data");
|
||||
}
|
||||
}
|
||||
@ -674,7 +677,6 @@ try
|
||||
{
|
||||
Log.Information("BCards application started successfully on {Hostname}", hostname);
|
||||
|
||||
// 🔥 CORREÇÃO 13: Aguardar um pouco para logs serem enviados antes da aplicação rodar
|
||||
if (isDevelopment)
|
||||
{
|
||||
Console.WriteLine("[DEBUG] Aguardando envio de logs iniciais...");
|
||||
@ -693,10 +695,8 @@ finally
|
||||
{
|
||||
Log.Information("BCards application shutting down on {Hostname}", hostname);
|
||||
|
||||
// 🔥 CORREÇÃO 14: Aguardar logs finais serem enviados
|
||||
await Task.Delay(5000);
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
// Make Program accessible for integration tests
|
||||
public partial class Program { }
|
||||
@ -119,9 +119,12 @@
|
||||
<input type="file" class="form-control" id="profileImageInput" name="ProfileImageFile" accept="image/*">
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Formatos aceitos: JPG, PNG, GIF. Máximo: 2MB e 4000x4000px. Será redimensionada para 400x400px.
|
||||
Formatos aceitos: JPG, PNG, GIF. Máximo: 2MB e 4000x4000px.
|
||||
</div>
|
||||
<span class="text-danger" id="imageError"></span>
|
||||
<div class="invalid-feedback" id="imageErrorFeedback" style="display: none;">
|
||||
Erro com a imagem selecionada
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@ -245,7 +248,9 @@
|
||||
"twitter",
|
||||
"instagram"
|
||||
};
|
||||
var match = myList.FirstOrDefault(stringToCheck => Model.Links[i].Icon.Contains(stringToCheck));
|
||||
var match = myList.FirstOrDefault(stringToCheck =>
|
||||
!string.IsNullOrEmpty(Model.Links[i].Icon) &&
|
||||
Model.Links[i].Icon.Contains(stringToCheck));
|
||||
if (match==null)
|
||||
{
|
||||
if (Model.Links[i].Type==LinkType.Normal)
|
||||
@ -369,10 +374,10 @@
|
||||
</div>
|
||||
</div>
|
||||
@{
|
||||
var facebook = Model.Links.Where(x => x.Icon.Contains("facebook")).FirstOrDefault();
|
||||
var twitter = Model.Links.Where(x => x.Icon.Contains("twitter")).FirstOrDefault();
|
||||
var whatsapp = Model.Links.Where(x => x.Icon.Contains("whatsapp")).FirstOrDefault();
|
||||
var instagram = Model.Links.Where(x => x.Icon.Contains("instagram")).FirstOrDefault();
|
||||
var facebook = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("facebook")).FirstOrDefault();
|
||||
var twitter = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("twitter")).FirstOrDefault();
|
||||
var whatsapp = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("whatsapp")).FirstOrDefault();
|
||||
var instagram = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("instagram")).FirstOrDefault();
|
||||
var facebookUrl = facebook !=null ? facebook.Url : "";
|
||||
var twitterUrl = twitter !=null ? twitter.Url : "";
|
||||
var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","") : "";
|
||||
@ -946,6 +951,9 @@
|
||||
// Check for validation errors and show toast + open accordion
|
||||
checkValidationErrors();
|
||||
|
||||
// Check for server-side image errors
|
||||
checkServerImageErrors();
|
||||
|
||||
// Garantir que campos não marcados sejam string vazia ao submeter
|
||||
$('form').on('submit', function() {
|
||||
ensureUncheckedFieldsAreEmpty();
|
||||
@ -1376,6 +1384,42 @@
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Nova função para mostrar toast de erro com múltiplas mensagens
|
||||
function showErrorToast(errors) {
|
||||
if (!Array.isArray(errors)) {
|
||||
errors = [errors];
|
||||
}
|
||||
|
||||
const errorList = errors.map(error => `<li>${error}</li>`).join('');
|
||||
const toastHtml = `
|
||||
<div class="toast align-items-center text-white bg-danger border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong><i class="fas fa-exclamation-triangle me-2"></i>Erro de Validação</strong>
|
||||
<ul class="mb-0 mt-2 small">
|
||||
${errorList}
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!$('#toastContainer').length) {
|
||||
$('body').append('<div id="toastContainer" class="toast-container position-fixed top-0 end-0 p-3"></div>');
|
||||
}
|
||||
|
||||
const $toast = $(toastHtml);
|
||||
$('#toastContainer').append($toast);
|
||||
|
||||
const toast = new bootstrap.Toast($toast[0], { delay: 6000 }); // 6 segundos para erro
|
||||
toast.show();
|
||||
|
||||
setTimeout(() => {
|
||||
$toast.remove();
|
||||
}, 7000);
|
||||
}
|
||||
|
||||
// Validation Error Handling
|
||||
function checkValidationErrors() {
|
||||
// Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado)
|
||||
@ -1663,19 +1707,24 @@
|
||||
// Preview da imagem selecionada
|
||||
fileInput.on('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
errorSpan.text('');
|
||||
const feedbackDiv = $('#imageErrorFeedback');
|
||||
|
||||
// Limpar estados de erro anteriores
|
||||
clearImageError();
|
||||
|
||||
if (!file) return;
|
||||
|
||||
// Validações client-side
|
||||
// Validações client-side com feedback visual melhorado
|
||||
if (!file.type.match(/^image\/(jpeg|jpg|png|gif)$/i)) {
|
||||
errorSpan.text('Formato inválido. Use apenas JPG, PNG ou GIF.');
|
||||
setImageError('Formato inválido. Use apenas JPG, PNG ou GIF.');
|
||||
showErrorToast(['Formato de imagem inválido. Use apenas JPG, PNG ou GIF.']);
|
||||
fileInput.val('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 2 * 1024 * 1024) { // 2MB
|
||||
errorSpan.text('Arquivo muito grande. Máximo 2MB.');
|
||||
setImageError('Arquivo muito grande. Máximo 2MB.');
|
||||
showErrorToast(['Arquivo muito grande. O tamanho máximo é de 2MB.']);
|
||||
fileInput.val('');
|
||||
return;
|
||||
}
|
||||
@ -1694,7 +1743,7 @@
|
||||
removeBtn.on('click', function() {
|
||||
if (confirm('Tem certeza que deseja remover a imagem de perfil?')) {
|
||||
fileInput.val('');
|
||||
hiddenField.val('');
|
||||
hiddenField.val('REMOVE_IMAGE'); // Valor específico para indicar remoção
|
||||
preview.attr('src', '/images/default-avatar.svg');
|
||||
removeBtn.hide();
|
||||
errorSpan.text('');
|
||||
@ -1706,6 +1755,59 @@
|
||||
removeBtn.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Funções auxiliares para gerenciar estados de erro da imagem
|
||||
function setImageError(message) {
|
||||
const fileInput = $('#profileImageInput');
|
||||
const errorSpan = $('#imageError');
|
||||
const feedbackDiv = $('#imageErrorFeedback');
|
||||
|
||||
// Adicionar classes de erro
|
||||
fileInput.addClass('is-invalid');
|
||||
|
||||
// Mostrar mensagens de erro
|
||||
errorSpan.text(message);
|
||||
feedbackDiv.text(message).show();
|
||||
|
||||
// Adicionar borda vermelha na área de preview
|
||||
$('.profile-image-preview').addClass('border-danger');
|
||||
}
|
||||
|
||||
function clearImageError() {
|
||||
const fileInput = $('#profileImageInput');
|
||||
const errorSpan = $('#imageError');
|
||||
const feedbackDiv = $('#imageErrorFeedback');
|
||||
|
||||
// Remover classes de erro
|
||||
fileInput.removeClass('is-invalid is-valid');
|
||||
|
||||
// Limpar mensagens de erro
|
||||
errorSpan.text('');
|
||||
feedbackDiv.hide();
|
||||
|
||||
// Remover borda vermelha da área de preview
|
||||
$('.profile-image-preview').removeClass('border-danger');
|
||||
}
|
||||
|
||||
// Função para verificar erros de imagem do servidor
|
||||
function checkServerImageErrors() {
|
||||
@if (TempData["ImageError"] != null)
|
||||
{
|
||||
@:const imageError = '@Html.Raw(TempData["ImageError"])';
|
||||
@:setImageError(imageError);
|
||||
@:showErrorToast([imageError]);
|
||||
}
|
||||
|
||||
// Verificar ModelState errors para ProfileImageFile
|
||||
@if (ViewData.ModelState.ContainsKey("ProfileImageFile"))
|
||||
{
|
||||
@:const modelStateError = '@Html.Raw(ViewData.ModelState["ProfileImageFile"].Errors.FirstOrDefault()?.ErrorMessage)';
|
||||
@:if (modelStateError) {
|
||||
@:setImageError(modelStateError);
|
||||
@:showErrorToast([modelStateError]);
|
||||
@:}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user