feat: add emergency admin rescue page and fix backup restoration to prevent lockouts
This commit is contained in:
@@ -87,8 +87,8 @@ export default function AdminLayout({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
// Pular verificação para a rota de emergência (suporta com ou sem prefixo de idioma)
|
// Pular verificação para rotas de emergência
|
||||||
if (pathname?.endsWith('/admin/backup/emergency')) {
|
if (pathname?.endsWith('/admin/backup/emergency') || pathname?.endsWith('/admin/rescue')) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -177,8 +177,8 @@ export default function AdminLayout({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se for a rota de emergência, renderiza apenas o conteúdo
|
// Se for uma rota de emergência, renderiza apenas o conteúdo
|
||||||
if (pathname?.endsWith('/admin/backup/emergency')) {
|
if (pathname?.endsWith('/admin/backup/emergency') || pathname?.endsWith('/admin/rescue')) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-900">
|
<div className="min-h-screen bg-slate-900">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
103
frontend/src/app/admin/rescue/page.tsx
Normal file
103
frontend/src/app/admin/rescue/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function RescuePage() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleCreate = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setStatus('loading');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/rescue', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password, name }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setStatus('success');
|
||||||
|
setMessage('Usuário administrador criado com sucesso! Redirecionando...');
|
||||||
|
setTimeout(() => router.push('/acesso'), 2000);
|
||||||
|
} else {
|
||||||
|
const error = await res.json();
|
||||||
|
setStatus('error');
|
||||||
|
setMessage(error.error || 'Erro ao criar usuário.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('error');
|
||||||
|
setMessage('Erro de conexão.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-900 flex items-center justify-center p-4">
|
||||||
|
<div className="max-w-md w-full bg-slate-800 rounded-2xl shadow-2xl p-8 border border-rose-500/30">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<i className="ri-alarm-warning-line text-3xl text-rose-500"></i>
|
||||||
|
<h1 className="text-2xl font-bold text-white">Resgate de Emergência</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-slate-400 mb-8 text-sm">Use esta página apenas se você foi bloqueado do sistema. Ela criará um novo administrador.</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleCreate} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-slate-400 uppercase mb-1">Nome Completo</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-4 py-3 text-white focus:ring-2 focus:ring-rose-500 outline-none"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Ex: Erik"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-slate-400 uppercase mb-1">E-mail de Acesso</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-4 py-3 text-white focus:ring-2 focus:ring-rose-500 outline-none"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="seu@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-slate-400 uppercase mb-1">Nova Senha</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-4 py-3 text-white focus:ring-2 focus:ring-rose-500 outline-none"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={status === 'loading'}
|
||||||
|
className="w-full bg-rose-600 hover:bg-rose-500 text-white font-bold py-4 rounded-xl transition-all shadow-lg flex items-center justify-center gap-3 mt-4"
|
||||||
|
>
|
||||||
|
{status === 'loading' ? 'Criando...' : 'Criar Administrador Agora'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div className={`p-4 rounded-lg text-center text-sm font-medium mt-4 ${status === 'success' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-rose-500/20 text-rose-400'
|
||||||
|
}`}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -146,15 +146,23 @@ export async function POST(req: NextRequest) {
|
|||||||
// Importação atômica com timeout estendido para 120 segundos
|
// Importação atômica com timeout estendido para 120 segundos
|
||||||
console.log('🔄 Iniciando transação no banco de dados...');
|
console.log('🔄 Iniciando transação no banco de dados...');
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
console.log('🧹 Limpando tabelas atuais...');
|
console.log('🧹 Limpando tabelas atuais (exceto usuários se o backup estiver incompleto)...');
|
||||||
await tx.message.deleteMany();
|
await tx.message.deleteMany();
|
||||||
await tx.project.deleteMany();
|
await tx.project.deleteMany();
|
||||||
await tx.service.deleteMany();
|
await tx.service.deleteMany();
|
||||||
await tx.pageContent.deleteMany();
|
await tx.pageContent.deleteMany();
|
||||||
|
|
||||||
|
// Só apaga os usuários se viermos com novos usuários no ZIP
|
||||||
|
const hasUsersInBackup = data.users && data.users.length > 0;
|
||||||
|
if (hasUsersInBackup) {
|
||||||
|
console.log('👥 Backup contém usuários. Atualizando tabela de usuários...');
|
||||||
await tx.user.deleteMany();
|
await tx.user.deleteMany();
|
||||||
|
await tx.user.createMany({ data: data.users });
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Backup NÃO contém usuários. Preservando usuários atuais para evitar bloqueio.');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('📝 Inserindo novos dados...');
|
console.log('📝 Inserindo novos dados...');
|
||||||
if (data.users?.length > 0) await tx.user.createMany({ data: data.users });
|
|
||||||
if (data.projects?.length > 0) await tx.project.createMany({ data: data.projects });
|
if (data.projects?.length > 0) await tx.project.createMany({ data: data.projects });
|
||||||
if (data.services?.length > 0) await tx.service.createMany({ data: data.services });
|
if (data.services?.length > 0) await tx.service.createMany({ data: data.services });
|
||||||
if (data.pageContents?.length > 0) await tx.pageContent.createMany({ data: data.pageContents });
|
if (data.pageContents?.length > 0) await tx.pageContent.createMany({ data: data.pageContents });
|
||||||
|
|||||||
35
frontend/src/app/api/admin/rescue/route.ts
Normal file
35
frontend/src/app/api/admin/rescue/route.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { email, password, name } = await req.json();
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criptografar senha
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
// Criar ou atualizar usuário
|
||||||
|
const user = await prisma.user.upsert({
|
||||||
|
where: { email },
|
||||||
|
update: {
|
||||||
|
password: hashedPassword,
|
||||||
|
name: name || undefined
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
name: name || 'Administrador'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: 'Usuário configurado com sucesso' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro no resgate de usuário:', error);
|
||||||
|
return NextResponse.json({ error: 'Erro interno no servidor' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user