353 lines
16 KiB
TypeScript
353 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Link from "next/link";
|
|
import { Button, Input, Checkbox } from "@/components/ui";
|
|
import { saveAuth, isAuthenticated, getToken, clearAuth } from '@/lib/auth';
|
|
import { API_ENDPOINTS } from '@/lib/api';
|
|
import dynamic from 'next/dynamic';
|
|
import { LoginBranding } from '@/components/auth/LoginBranding';
|
|
import {
|
|
EnvelopeIcon,
|
|
LockClosedIcon,
|
|
ShieldCheckIcon,
|
|
BoltIcon,
|
|
UserGroupIcon,
|
|
ChartBarIcon,
|
|
ExclamationCircleIcon,
|
|
CheckCircleIcon
|
|
} from "@heroicons/react/24/outline";
|
|
|
|
const ThemeToggle = dynamic(() => import('@/components/ThemeToggle'), { ssr: false });
|
|
|
|
export default function LoginPage() {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isSuperAdmin, setIsSuperAdmin] = useState(false);
|
|
const [subdomain, setSubdomain] = useState<string>('');
|
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
|
const [successMessage, setSuccessMessage] = useState<string>('');
|
|
const [formData, setFormData] = useState({
|
|
email: "",
|
|
password: "",
|
|
rememberMe: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
const hostname = window.location.hostname;
|
|
const sub = hostname.split('.')[0];
|
|
const superAdmin = sub === 'dash';
|
|
setSubdomain(sub);
|
|
setIsSuperAdmin(superAdmin);
|
|
|
|
// Verificar se tem parâmetro de erro de tenant não encontrado
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.get('error') === 'tenant_not_found') {
|
|
console.log('⚠️ Tenant não encontrado, limpando autenticação...');
|
|
clearAuth();
|
|
localStorage.removeItem('agency-logo-url');
|
|
localStorage.removeItem('agency-primary-color');
|
|
localStorage.removeItem('agency-secondary-color');
|
|
setErrorMessage('Esta agência não existe mais ou foi desativada.');
|
|
return;
|
|
}
|
|
|
|
if (isAuthenticated()) {
|
|
// Validar token antes de redirecionar para evitar loops
|
|
const token = getToken();
|
|
fetch(API_ENDPOINTS.me, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
})
|
|
.then(res => {
|
|
if (res.ok) {
|
|
const target = superAdmin ? '/superadmin' : '/dashboard';
|
|
window.location.href = target;
|
|
} else {
|
|
// Token inválido ou expirado
|
|
clearAuth();
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error('Erro ao validar sessão:', err);
|
|
// Em caso de erro de rede, não redireciona nem limpa, deixa o usuário tentar logar
|
|
});
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setErrorMessage('');
|
|
setSuccessMessage('');
|
|
|
|
// Validações do lado do cliente
|
|
if (!formData.email) {
|
|
setErrorMessage('Por favor, insira seu email para continuar.');
|
|
return;
|
|
}
|
|
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
setErrorMessage('Ops! O formato do email não parece correto. Por favor, verifique e tente novamente.');
|
|
return;
|
|
}
|
|
|
|
if (!formData.password) {
|
|
setErrorMessage('Por favor, insira sua senha para acessar sua conta.');
|
|
return;
|
|
}
|
|
|
|
if (formData.password.length < 3) {
|
|
setErrorMessage('A senha parece muito curta. Por favor, verifique se digitou corretamente.');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
email: formData.email,
|
|
password: formData.password,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
|
|
// Mensagens humanizadas para cada tipo de erro
|
|
if (response.status === 401 || response.status === 403) {
|
|
setErrorMessage('Email ou senha incorretos. Por favor, verifique seus dados e tente novamente.');
|
|
} else if (response.status >= 500) {
|
|
setErrorMessage('Estamos com problemas no servidor no momento. Por favor, tente novamente em alguns instantes.');
|
|
} else {
|
|
setErrorMessage(error.message || 'Algo deu errado ao tentar fazer login. Por favor, tente novamente.');
|
|
}
|
|
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
saveAuth(data.token, data.user);
|
|
|
|
console.log('Login successful:', data.user);
|
|
|
|
setSuccessMessage('Login realizado com sucesso! Redirecionando você agora...');
|
|
|
|
setTimeout(() => {
|
|
const target = isSuperAdmin ? '/superadmin' : '/dashboard';
|
|
window.location.href = target;
|
|
}, 1000);
|
|
} catch (error: any) {
|
|
console.error('Login error:', error);
|
|
setErrorMessage('Não conseguimos conectar ao servidor. Verifique sua conexão com a internet e tente novamente.');
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Script inline para aplicar cor primária ANTES do React */}
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
(function() {
|
|
try {
|
|
const cachedPrimary = localStorage.getItem('agency-primary-color');
|
|
if (cachedPrimary) {
|
|
function hexToRgb(hex) {
|
|
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(cachedPrimary);
|
|
|
|
if (primaryRgb) {
|
|
const root = document.documentElement;
|
|
root.style.setProperty('--brand-color', cachedPrimary);
|
|
root.style.setProperty('--gradient', 'linear-gradient(135deg, ' + cachedPrimary + ', ' + cachedPrimary + ')');
|
|
root.style.setProperty('--brand-rgb', primaryRgb);
|
|
root.style.setProperty('--brand-strong-rgb', primaryRgb);
|
|
root.style.setProperty('--brand-hover-rgb', primaryRgb);
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
})();
|
|
`,
|
|
}}
|
|
/>
|
|
<LoginBranding />
|
|
<div className="flex min-h-screen">
|
|
{/* Lado Esquerdo - Formulário */}
|
|
<div className="w-full lg:w-1/2 flex items-center justify-center px-6 sm:px-12 py-12">
|
|
<div className="w-full max-w-md">
|
|
{/* Logo mobile */}
|
|
<div className="lg:hidden text-center mb-8">
|
|
<div className="inline-block px-6 py-3 rounded-2xl" style={{ background: 'var(--brand-color)' }}>
|
|
<h1 className="text-3xl font-bold text-white">
|
|
{isSuperAdmin ? 'aggios' : subdomain}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Theme Toggle */}
|
|
<div className="flex justify-end mb-4">
|
|
<ThemeToggle />
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h2 className="text-[28px] font-bold text-[#000000] dark:text-white">
|
|
{isSuperAdmin ? 'Painel Administrativo' : 'Bem-vindo de volta'}
|
|
</h2>
|
|
<p className="text-[14px] text-[#7D7D7D] dark:text-gray-400 mt-2">
|
|
{isSuperAdmin
|
|
? 'Acesso exclusivo para administradores Aggios'
|
|
: 'Entre com suas credenciais para acessar o painel'
|
|
}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Form */}
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
{/* Mensagem de Erro */}
|
|
{errorMessage && (
|
|
<div className="flex items-start gap-3 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
|
|
<ExclamationCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
|
<p className="text-sm text-red-800 dark:text-red-300 leading-relaxed">
|
|
{errorMessage}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mensagem de Sucesso */}
|
|
{successMessage && (
|
|
<div className="flex items-start gap-3 p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
|
|
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
|
<p className="text-sm text-green-800 dark:text-green-300 leading-relaxed">
|
|
{successMessage}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<Input
|
|
label="Email"
|
|
type="email"
|
|
placeholder="seu@email.com"
|
|
leftIcon={<EnvelopeIcon className="w-5 h-5" />}
|
|
value={formData.email}
|
|
onChange={(e) => {
|
|
setFormData({ ...formData, email: e.target.value });
|
|
setErrorMessage(''); // Limpa o erro ao digitar
|
|
}}
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
label="Senha"
|
|
type="password"
|
|
placeholder="Digite sua senha"
|
|
leftIcon={<LockClosedIcon className="w-5 h-5" />}
|
|
value={formData.password}
|
|
onChange={(e) => {
|
|
setFormData({ ...formData, password: e.target.value });
|
|
setErrorMessage(''); // Limpa o erro ao digitar
|
|
}}
|
|
required
|
|
/>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Checkbox
|
|
id="rememberMe"
|
|
label="Lembrar de mim"
|
|
checked={formData.rememberMe}
|
|
onChange={(e) => setFormData({ ...formData, rememberMe: e.target.checked })}
|
|
/>
|
|
<Link
|
|
href="/recuperar-senha"
|
|
className="text-[14px] font-medium hover:opacity-80 transition-opacity"
|
|
style={{ color: 'var(--brand-color)' }}
|
|
>
|
|
Esqueceu a senha?
|
|
</Link>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
size="lg"
|
|
className="w-full"
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? 'Entrando...' : 'Entrar'}
|
|
</Button>
|
|
|
|
{/* Link para cadastro - apenas para agências */}
|
|
{!isSuperAdmin && (
|
|
<p className="text-center text-[14px] text-[#7D7D7D] dark:text-gray-400">
|
|
Ainda não tem conta?{' '}
|
|
<a
|
|
href="http://dash.localhost/cadastro"
|
|
className="font-medium hover:opacity-80 transition-opacity"
|
|
style={{ color: 'var(--brand-color)' }}
|
|
>
|
|
Cadastre sua agência
|
|
</a>
|
|
</p>
|
|
)}
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Lado Direito - Branding */}
|
|
<div className="hidden lg:flex lg:w-1/2 relative overflow-hidden" style={{ background: 'var(--brand-color)' }}>
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center p-12 text-white">
|
|
<div className="max-w-md text-center">
|
|
<h1 className="text-5xl font-bold mb-6">
|
|
{isSuperAdmin ? 'aggios' : subdomain}
|
|
</h1>
|
|
<p className="text-xl opacity-90 mb-8">
|
|
{isSuperAdmin
|
|
? 'Gerencie todas as agências em um só lugar'
|
|
: 'Gerencie seus clientes com eficiência'
|
|
}
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-6 text-left">
|
|
<div>
|
|
<ShieldCheckIcon className="w-8 h-8 mb-2" />
|
|
<h3 className="font-semibold mb-1">Seguro</h3>
|
|
<p className="text-sm opacity-80">Proteção de dados</p>
|
|
</div>
|
|
<div>
|
|
<BoltIcon className="w-8 h-8 mb-2" />
|
|
<h3 className="font-semibold mb-1">Rápido</h3>
|
|
<p className="text-sm opacity-80">Performance otimizada</p>
|
|
</div>
|
|
<div>
|
|
<UserGroupIcon className="w-8 h-8 mb-2" />
|
|
<h3 className="font-semibold mb-1">Colaborativo</h3>
|
|
<p className="text-sm opacity-80">Trabalho em equipe</p>
|
|
</div>
|
|
<div>
|
|
<ChartBarIcon className="w-8 h-8 mb-2" />
|
|
<h3 className="font-semibold mb-1">Insights</h3>
|
|
<p className="text-sm opacity-80">Relatórios detalhados</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|