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(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)",
|
||||||
"Bash(sed:*)",
|
"Bash(sed:*)",
|
||||||
"Bash(./clean-build.sh:*)",
|
"Bash(./clean-build.sh:*)",
|
||||||
"Bash(git add:*)"
|
"Bash(git add:*)",
|
||||||
|
"Bash(scp:*)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": false
|
"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]
|
[HttpPost]
|
||||||
[Route("ManagePage")]
|
[Route("ManagePage")]
|
||||||
|
[RequestSizeLimit(5 * 1024 * 1024)] // Allow 5MB uploads
|
||||||
|
[RequestFormLimits(MultipartBodyLengthLimit = 5 * 1024 * 1024)]
|
||||||
public async Task<IActionResult> ManagePage(ManagePageViewModel model)
|
public async Task<IActionResult> ManagePage(ManagePageViewModel model)
|
||||||
{
|
{
|
||||||
ViewBag.IsHomePage = false;
|
ViewBag.IsHomePage = false;
|
||||||
@ -231,10 +233,27 @@ public class AdminController : Controller
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error uploading profile image");
|
_logger.LogError(ex, "Error uploading profile image");
|
||||||
ModelState.AddModelError("ProfileImageFile", "Erro ao fazer upload da imagem. Tente novamente.");
|
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.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
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);
|
return View(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,10 +347,20 @@ public class AdminController : Controller
|
|||||||
return RedirectToAction("Dashboard");
|
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)
|
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);
|
await UpdateUserPageFromModel(existingPage, model);
|
||||||
|
|||||||
@ -23,6 +23,14 @@ namespace BCards.Web.Middleware
|
|||||||
{
|
{
|
||||||
await _next(context);
|
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)
|
// Aplicar headers apenas para páginas HTML (não APIs, imagens, etc)
|
||||||
if (context.Response.ContentType?.StartsWith("text/html") == true)
|
if (context.Response.ContentType?.StartsWith("text/html") == true)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,12 +4,10 @@ using BCards.Web.Repositories;
|
|||||||
using BCards.Web.HealthChecks;
|
using BCards.Web.HealthChecks;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.Google;
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
|
||||||
using Microsoft.AspNetCore.Localization;
|
using Microsoft.AspNetCore.Localization;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Stripe;
|
|
||||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||||
using SendGrid;
|
using SendGrid;
|
||||||
using BCards.Web.Middleware;
|
using BCards.Web.Middleware;
|
||||||
@ -22,11 +20,9 @@ using Serilog.Sinks.OpenSearch;
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Configure Serilog with environment-specific settings
|
|
||||||
var isDevelopment = builder.Environment.IsDevelopment();
|
var isDevelopment = builder.Environment.IsDevelopment();
|
||||||
var hostname = Environment.MachineName;
|
var hostname = Environment.MachineName;
|
||||||
|
|
||||||
// 🔥 CORREÇÃO 1: Habilitar SelfLog ANTES de configurar o logger
|
|
||||||
Serilog.Debugging.SelfLog.Enable(msg =>
|
Serilog.Debugging.SelfLog.Enable(msg =>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[SERILOG SELF] {DateTime.Now:HH:mm:ss} {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.AspNetCore.StaticFiles", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||||
// 🔥 GARANTIR CONSOLE EM PRODUÇÃO TAMBÉM
|
|
||||||
.WriteTo.Console(
|
.WriteTo.Console(
|
||||||
restrictedToMinimumLevel: LogEventLevel.Information,
|
restrictedToMinimumLevel: LogEventLevel.Information,
|
||||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
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();
|
var logger = loggerConfig.CreateLogger();
|
||||||
Log.Logger = logger;
|
Log.Logger = logger;
|
||||||
|
|
||||||
@ -176,7 +171,6 @@ builder.Host.UseSerilog();
|
|||||||
// Log startup information
|
// Log startup information
|
||||||
Log.Information("Starting BCards application on {Hostname} in {Environment} mode", hostname, builder.Environment.EnvironmentName);
|
Log.Information("Starting BCards application on {Hostname} in {Environment} mode", hostname, builder.Environment.EnvironmentName);
|
||||||
|
|
||||||
// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER
|
|
||||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
{
|
{
|
||||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
@ -186,7 +180,6 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|||||||
options.ForwardLimit = null;
|
options.ForwardLimit = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔥 OTIMIZAÇÃO: Sistema de Compressão de Response (Brotli + Gzip)
|
|
||||||
builder.Services.AddResponseCompression(options =>
|
builder.Services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
options.EnableForHttps = true;
|
options.EnableForHttps = true;
|
||||||
@ -410,12 +403,25 @@ builder.Services.AddScoped<IEmailService, EmailService>();
|
|||||||
|
|
||||||
builder.Services.AddScoped<IImageStorageService, GridFSImageStorage>();
|
builder.Services.AddScoped<IImageStorageService, GridFSImageStorage>();
|
||||||
|
|
||||||
|
// Configure upload limits for file handling (images up to 5MB)
|
||||||
builder.Services.Configure<FormOptions>(options =>
|
builder.Services.Configure<FormOptions>(options =>
|
||||||
{
|
{
|
||||||
options.MultipartBodyLengthLimit = 10 * 1024 * 1024;
|
options.MultipartBodyLengthLimit = 5 * 1024 * 1024; // 5MB
|
||||||
options.ValueLengthLimit = int.MaxValue;
|
options.ValueLengthLimit = int.MaxValue;
|
||||||
options.ValueCountLimit = int.MaxValue;
|
options.ValueCountLimit = int.MaxValue;
|
||||||
options.KeyLengthLimit = 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>();
|
builder.Services.AddScoped<ILivePageRepository, LivePageRepository>();
|
||||||
@ -555,11 +561,11 @@ app.UseRequestLocalization();
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.UseMiddleware<BCards.Web.Middleware.SmartCacheMiddleware>();
|
app.UseMiddleware<SmartCacheMiddleware>();
|
||||||
app.UseMiddleware<BCards.Web.Middleware.AuthCacheMiddleware>();
|
app.UseMiddleware<AuthCacheMiddleware>();
|
||||||
app.UseMiddleware<BCards.Web.Middleware.PlanLimitationMiddleware>();
|
app.UseMiddleware<PlanLimitationMiddleware>();
|
||||||
app.UseMiddleware<BCards.Web.Middleware.PageStatusMiddleware>();
|
app.UseMiddleware<PageStatusMiddleware>();
|
||||||
app.UseMiddleware<BCards.Web.Middleware.PreviewTokenMiddleware>();
|
app.UseMiddleware<PreviewTokenMiddleware>();
|
||||||
app.UseMiddleware<ModerationAuthMiddleware>();
|
app.UseMiddleware<ModerationAuthMiddleware>();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
@ -640,7 +646,6 @@ app.MapControllerRoute(
|
|||||||
name: "default",
|
name: "default",
|
||||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
// Initialize default data
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var themeService = scope.ServiceProvider.GetRequiredService<IThemeService>();
|
var themeService = scope.ServiceProvider.GetRequiredService<IThemeService>();
|
||||||
@ -660,12 +665,10 @@ using (var scope = app.Services.CreateScope())
|
|||||||
await categoryService.InitializeDefaultCategoriesAsync();
|
await categoryService.InitializeDefaultCategoriesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 CORREÇÃO 12: Logs adicionais após inicialização
|
|
||||||
Log.Information("Default themes and categories initialized successfully");
|
Log.Information("Default themes and categories initialized successfully");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
//var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
|
|
||||||
Log.Error(ex, "Error initializing default data");
|
Log.Error(ex, "Error initializing default data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -674,7 +677,6 @@ try
|
|||||||
{
|
{
|
||||||
Log.Information("BCards application started successfully on {Hostname}", hostname);
|
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)
|
if (isDevelopment)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[DEBUG] Aguardando envio de logs iniciais...");
|
Console.WriteLine("[DEBUG] Aguardando envio de logs iniciais...");
|
||||||
@ -693,10 +695,8 @@ finally
|
|||||||
{
|
{
|
||||||
Log.Information("BCards application shutting down on {Hostname}", hostname);
|
Log.Information("BCards application shutting down on {Hostname}", hostname);
|
||||||
|
|
||||||
// 🔥 CORREÇÃO 14: Aguardar logs finais serem enviados
|
|
||||||
await Task.Delay(5000);
|
await Task.Delay(5000);
|
||||||
Log.CloseAndFlush();
|
Log.CloseAndFlush();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make Program accessible for integration tests
|
|
||||||
public partial class Program { }
|
public partial class Program { }
|
||||||
@ -222,4 +222,4 @@ public class GridFSImageStorage : IImageStorageService
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,9 +119,12 @@
|
|||||||
<input type="file" class="form-control" id="profileImageInput" name="ProfileImageFile" accept="image/*">
|
<input type="file" class="form-control" id="profileImageInput" name="ProfileImageFile" accept="image/*">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<i class="fas fa-info-circle me-1"></i>
|
<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>
|
</div>
|
||||||
<span class="text-danger" id="imageError"></span>
|
<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>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@ -245,7 +248,9 @@
|
|||||||
"twitter",
|
"twitter",
|
||||||
"instagram"
|
"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 (match==null)
|
||||||
{
|
{
|
||||||
if (Model.Links[i].Type==LinkType.Normal)
|
if (Model.Links[i].Type==LinkType.Normal)
|
||||||
@ -369,10 +374,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@{
|
@{
|
||||||
var facebook = Model.Links.Where(x => x.Icon.Contains("facebook")).FirstOrDefault();
|
var facebook = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("facebook")).FirstOrDefault();
|
||||||
var twitter = Model.Links.Where(x => x.Icon.Contains("twitter")).FirstOrDefault();
|
var twitter = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("twitter")).FirstOrDefault();
|
||||||
var whatsapp = Model.Links.Where(x => x.Icon.Contains("whatsapp")).FirstOrDefault();
|
var whatsapp = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("whatsapp")).FirstOrDefault();
|
||||||
var instagram = Model.Links.Where(x => x.Icon.Contains("instagram")).FirstOrDefault();
|
var instagram = Model.Links.Where(x => !string.IsNullOrEmpty(x.Icon) && x.Icon.Contains("instagram")).FirstOrDefault();
|
||||||
var facebookUrl = facebook !=null ? facebook.Url : "";
|
var facebookUrl = facebook !=null ? facebook.Url : "";
|
||||||
var twitterUrl = twitter !=null ? twitter.Url : "";
|
var twitterUrl = twitter !=null ? twitter.Url : "";
|
||||||
var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","") : "";
|
var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","") : "";
|
||||||
@ -946,6 +951,9 @@
|
|||||||
// Check for validation errors and show toast + open accordion
|
// Check for validation errors and show toast + open accordion
|
||||||
checkValidationErrors();
|
checkValidationErrors();
|
||||||
|
|
||||||
|
// Check for server-side image errors
|
||||||
|
checkServerImageErrors();
|
||||||
|
|
||||||
// Garantir que campos não marcados sejam string vazia ao submeter
|
// Garantir que campos não marcados sejam string vazia ao submeter
|
||||||
$('form').on('submit', function() {
|
$('form').on('submit', function() {
|
||||||
ensureUncheckedFieldsAreEmpty();
|
ensureUncheckedFieldsAreEmpty();
|
||||||
@ -1376,6 +1384,42 @@
|
|||||||
}, 5000);
|
}, 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
|
// Validation Error Handling
|
||||||
function checkValidationErrors() {
|
function checkValidationErrors() {
|
||||||
// Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado)
|
// Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado)
|
||||||
@ -1663,19 +1707,24 @@
|
|||||||
// Preview da imagem selecionada
|
// Preview da imagem selecionada
|
||||||
fileInput.on('change', function(e) {
|
fileInput.on('change', function(e) {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
errorSpan.text('');
|
const feedbackDiv = $('#imageErrorFeedback');
|
||||||
|
|
||||||
|
// Limpar estados de erro anteriores
|
||||||
|
clearImageError();
|
||||||
|
|
||||||
if (!file) return;
|
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)) {
|
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('');
|
fileInput.val('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > 2 * 1024 * 1024) { // 2MB
|
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('');
|
fileInput.val('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1694,7 +1743,7 @@
|
|||||||
removeBtn.on('click', function() {
|
removeBtn.on('click', function() {
|
||||||
if (confirm('Tem certeza que deseja remover a imagem de perfil?')) {
|
if (confirm('Tem certeza que deseja remover a imagem de perfil?')) {
|
||||||
fileInput.val('');
|
fileInput.val('');
|
||||||
hiddenField.val('');
|
hiddenField.val('REMOVE_IMAGE'); // Valor específico para indicar remoção
|
||||||
preview.attr('src', '/images/default-avatar.svg');
|
preview.attr('src', '/images/default-avatar.svg');
|
||||||
removeBtn.hide();
|
removeBtn.hide();
|
||||||
errorSpan.text('');
|
errorSpan.text('');
|
||||||
@ -1706,6 +1755,59 @@
|
|||||||
removeBtn.show();
|
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>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user