v1.4: Segurança multi-tenant, file serving via API e UX humanizada

-  Validação cross-tenant no login e rotas protegidas
-  File serving via /api/files/{bucket}/{path} (eliminação DNS)
-  Mensagens de erro humanizadas inline (sem pop-ups)
-  Middleware tenant detection via headers customizados
-  Upload de logos retorna URLs via API
-  README atualizado com changelog v1.4 completo
This commit is contained in:
Erik Silva
2025-12-13 15:05:51 -03:00
parent 04c954c3d9
commit 2f1cf2bb2a
42 changed files with 2215 additions and 872 deletions

View File

@@ -1,25 +1,53 @@
'use client';
import { ReactNode } from 'react';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #ff3a05, #ff0080)';
// Helper to lighten color
const lightenColor = (color: string, percent: number) => {
const num = parseInt(color.replace("#", ""), 16),
amt = Math.round(2.55 * percent),
R = (num >> 16) + amt,
B = ((num >> 8) & 0x00ff) + amt,
G = (num & 0x0000ff) + amt;
return (
"#" +
(
0x1000000 +
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
(B < 255 ? (B < 1 ? 0 : B) : 255) * 0x100 +
(G < 255 ? (G < 1 ? 0 : G) : 255)
)
.toString(16)
.slice(1)
);
};
const setGradientVariables = (gradient: string) => {
document.documentElement.style.setProperty('--gradient-primary', gradient);
document.documentElement.style.setProperty('--gradient', gradient);
document.documentElement.style.setProperty('--gradient-text', gradient.replace('90deg', 'to right'));
document.documentElement.style.setProperty('--color-gradient-brand', gradient.replace('90deg', 'to right'));
const setBrandColors = (primary: string, secondary: string) => {
document.documentElement.style.setProperty('--brand-color', primary);
document.documentElement.style.setProperty('--brand-color-strong', secondary);
// Create a lighter version of primary for hover
const primaryLight = lightenColor(primary, 20); // Lighten by 20%
document.documentElement.style.setProperty('--brand-color-hover', primaryLight);
// Set RGB variables if needed by other components
const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}` : null;
};
const primaryRgb = hexToRgb(primary);
const secondaryRgb = hexToRgb(secondary);
const primaryLightRgb = hexToRgb(primaryLight);
if (primaryRgb) document.documentElement.style.setProperty('--brand-rgb', primaryRgb);
if (secondaryRgb) document.documentElement.style.setProperty('--brand-strong-rgb', secondaryRgb);
if (primaryLightRgb) document.documentElement.style.setProperty('--brand-hover-rgb', primaryLightRgb);
};
export default function LayoutWrapper({ children }: { children: ReactNode }) {
const pathname = usePathname();
useEffect(() => {
// Em toda troca de rota, volta para o tema padrão; layouts específicos (ex.: agência) aplicam o próprio na sequência
setGradientVariables(DEFAULT_GRADIENT);
}, [pathname]);
// Temporariamente desativado o carregamento dinâmico de cores/tema para eliminar possíveis
// efeitos colaterais de hidratação e 429 no middleware/backend. Se precisar reativar, mover
// para nível de servidor (next/head ou metadata) para evitar mutações de DOM no cliente.
return <>{children}</>;
}