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:
138
front-end-agency/components/auth/LoginBranding.tsx
Normal file
138
front-end-agency/components/auth/LoginBranding.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* LoginBranding - Aplica cor primária da agência na página de login
|
||||
* Busca cor do localStorage ou da API se não houver cache
|
||||
*/
|
||||
export function LoginBranding() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
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 applyTheme = (primary: string) => {
|
||||
if (!primary) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const primaryRgb = hexToRgb(primary);
|
||||
|
||||
root.style.setProperty('--brand-color', primary);
|
||||
root.style.setProperty('--gradient', `linear-gradient(135deg, ${primary}, ${primary})`);
|
||||
|
||||
if (primaryRgb) {
|
||||
root.style.setProperty('--brand-rgb', primaryRgb);
|
||||
root.style.setProperty('--brand-strong-rgb', primaryRgb);
|
||||
root.style.setProperty('--brand-hover-rgb', primaryRgb);
|
||||
}
|
||||
};
|
||||
|
||||
const updateFavicon = (url: string) => {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
|
||||
try {
|
||||
console.log('🎨 LoginBranding: Atualizando favicon para:', url);
|
||||
const newHref = `${url}${url.includes('?') ? '&' : '?'}v=${Date.now()}`;
|
||||
|
||||
// Buscar TODOS os links de ícone existentes
|
||||
const existingLinks = document.querySelectorAll("link[rel*='icon']");
|
||||
|
||||
if (existingLinks.length > 0) {
|
||||
// Atualizar href de todos os links existentes (SEM REMOVER)
|
||||
existingLinks.forEach(link => {
|
||||
link.setAttribute('href', newHref);
|
||||
});
|
||||
console.log(`✅ ${existingLinks.length} favicons atualizados`);
|
||||
} else {
|
||||
// Criar novo link apenas se não existir nenhum
|
||||
const newLink = document.createElement('link');
|
||||
newLink.rel = 'icon';
|
||||
newLink.type = 'image/x-icon';
|
||||
newLink.href = newHref;
|
||||
document.head.appendChild(newLink);
|
||||
console.log('✅ Novo favicon criado');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao atualizar favicon:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadBranding = async () => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
const subdomain = hostname.split('.')[0];
|
||||
|
||||
// Para dash.localhost ou localhost sem subdomínio, não buscar
|
||||
if (!subdomain || subdomain === 'localhost' || subdomain === 'www' || subdomain === 'dash') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Buscar DIRETO do backend (bypass da rota Next.js que está com problema)
|
||||
console.log('LoginBranding: Buscando cores para:', subdomain);
|
||||
const apiUrl = `/api/tenant/config?subdomain=${subdomain}`;
|
||||
console.log('LoginBranding: URL:', apiUrl);
|
||||
|
||||
const response = await fetch(apiUrl);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('LoginBranding: Dados recebidos:', data);
|
||||
|
||||
if (data.primary_color) {
|
||||
applyTheme(data.primary_color);
|
||||
localStorage.setItem('agency-primary-color', data.primary_color);
|
||||
console.log('LoginBranding: Cor aplicada!');
|
||||
}
|
||||
|
||||
if (data.logo_url) {
|
||||
updateFavicon(data.logo_url);
|
||||
localStorage.setItem('agency-logo-url', data.logo_url);
|
||||
console.log('LoginBranding: Favicon aplicado!');
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
console.error('LoginBranding: API retornou:', response.status);
|
||||
}
|
||||
|
||||
// 2. Fallback para cache
|
||||
console.log('LoginBranding: Tentando cache');
|
||||
const cachedPrimary = localStorage.getItem('agency-primary-color');
|
||||
const cachedLogo = localStorage.getItem('agency-logo-url');
|
||||
|
||||
if (cachedPrimary) {
|
||||
applyTheme(cachedPrimary);
|
||||
}
|
||||
if (cachedLogo) {
|
||||
updateFavicon(cachedLogo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LoginBranding: Erro:', error);
|
||||
const cachedPrimary = localStorage.getItem('agency-primary-color');
|
||||
const cachedLogo = localStorage.getItem('agency-logo-url');
|
||||
|
||||
if (cachedPrimary) {
|
||||
applyTheme(cachedPrimary);
|
||||
}
|
||||
if (cachedLogo) {
|
||||
updateFavicon(cachedLogo);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadBranding();
|
||||
}, [mounted]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user