feat: adicionar interface e rota para alterar credenciais de admin
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m37s Details

This commit is contained in:
Sidney 2026-06-07 20:38:41 -03:00
parent 9d13e6626d
commit baaa36f0a2
3 changed files with 206 additions and 26 deletions

View File

@ -0,0 +1,59 @@
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';
import bcrypt from 'bcryptjs';
export async function POST(req: Request) {
try {
const { oldPassword, newEmail, newPassword } = await req.json();
if (!oldPassword || !newEmail || !newPassword) {
return NextResponse.json({ error: 'Preencha todos os campos' }, { status: 400 });
}
// 1. Validate the old password
let isValidOld = false;
// Check if there is already a superadmin in DB
const res = await query(`SELECT * FROM users WHERE role = 'superadmin' LIMIT 1`);
if (res.rows.length > 0) {
// Compare with DB superadmin
const adminInDb = res.rows[0];
isValidOld = await bcrypt.compare(oldPassword, adminInDb.password_hash);
} else {
// If no superadmin in DB, compare with fallback ENV password
const ADMIN_PASS = process.env.ADMIN_PASSWORD || 'admin123';
if (oldPassword === ADMIN_PASS) {
isValidOld = true;
}
}
if (!isValidOld) {
return NextResponse.json({ error: 'Senha antiga incorreta' }, { status: 401 });
}
// 2. Hash new password
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(newPassword, salt);
// 3. Update or Insert superadmin
if (res.rows.length > 0) {
// Update existing
await query(`
UPDATE users SET email = $1, password_hash = $2 WHERE role = 'superadmin'
`, [newEmail, hash]);
} else {
// Insert new superadmin (tenant_id will be NULL)
await query(`
INSERT INTO users (name, email, password_hash, role)
VALUES ('Super Admin', $1, $2, 'superadmin')
`, [newEmail, hash]);
}
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('Change credentials error:', error);
return NextResponse.json({ error: 'Erro interno do servidor' }, { status: 500 });
}
}

View File

@ -1,4 +1,6 @@
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
export async function POST(req: Request) {
@ -6,26 +8,49 @@ export async function POST(req: Request) {
const { email, password } = await req.json();
if (!email || !password) return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
// Hardcoded Admin Credentials for SaaS owner
// In production, you'd use ENV variables like process.env.ADMIN_EMAIL
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@agendapro.com';
const ADMIN_PASS = process.env.ADMIN_PASSWORD || 'admin123';
let isValid = false;
let adminName = 'Super Admin';
let adminEmail = email;
if (email !== ADMIN_EMAIL || password !== ADMIN_PASS) {
// 1. Check if there is a superadmin in DB
const res = await query(`SELECT * FROM users WHERE role = 'superadmin' AND email = $1 LIMIT 1`, [email]);
if (res.rows.length > 0) {
// 2. Validate with DB
const adminInDb = res.rows[0];
isValid = await bcrypt.compare(password, adminInDb.password_hash);
adminName = adminInDb.name;
} else {
// 3. Fallback to Hardcoded Admin Credentials if NO superadmin exists in DB
// We check if ANY superadmin exists. If someone created a superadmin, the fallback is DISABLED for security!
const anySuperadmin = await query(`SELECT id FROM users WHERE role = 'superadmin' LIMIT 1`);
if (anySuperadmin.rows.length === 0) {
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@agendapro.com';
const ADMIN_PASS = process.env.ADMIN_PASSWORD || 'admin123';
if (email === ADMIN_EMAIL && password === ADMIN_PASS) {
isValid = true;
adminEmail = ADMIN_EMAIL;
}
}
}
if (!isValid) {
return NextResponse.json({ error: 'Credenciais inválidas' }, { status: 401 });
}
const secret = process.env.JWT_SECRET || 'fallback_secret_for_development';
const token = jwt.sign({
role: 'superadmin',
email: ADMIN_EMAIL
email: adminEmail
}, secret, { expiresIn: '7d' });
return NextResponse.json({
token,
user: {
name: 'Super Admin',
email: ADMIN_EMAIL,
name: adminName,
email: adminEmail,
role: 'superadmin'
}
});

View File

@ -1,13 +1,22 @@
'use client';
import { useState } from 'react';
import { Mail, Lock, Shield, ArrowRight } from 'lucide-react';
import { Mail, Lock, Shield, ArrowRight, Settings } from 'lucide-react';
import { useRouter } from 'next/navigation';
export default function AdminLoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// For changing credentials
const [showChangeCreds, setShowChangeCreds] = useState(false);
const [oldPassword, setOldPassword] = useState('');
const [newEmail, setNewEmail] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const router = useRouter();
const handleLogin = async (e: React.FormEvent) => {
@ -42,6 +51,47 @@ export default function AdminLoginPage() {
}
};
const handleChangeCredentials = async (e: React.FormEvent) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
setError('A nova senha e a confirmação não conferem.');
return;
}
setLoading(true);
setError('');
setSuccess('');
try {
const res = await fetch('/api/auth/change-credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oldPassword, newEmail, newPassword })
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Erro ao alterar credenciais');
setLoading(false);
return;
}
setSuccess('Credenciais alteradas com sucesso! Faça login com a nova senha.');
setShowChangeCreds(false);
setEmail(newEmail);
setPassword('');
setOldPassword('');
setNewPassword('');
setConfirmPassword('');
setLoading(false);
} catch (err) {
console.error(err);
setError('Falha de conexão com o servidor');
setLoading(false);
}
};
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-primary)' }}>
<div style={{ width: '100%', maxWidth: '420px', padding: '40px', background: 'var(--bg-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-xl)', border: '1px solid var(--border-color)', animation: 'slideUp 0.5s ease' }}>
@ -55,24 +105,70 @@ export default function AdminLoginPage() {
</div>
{error && <div style={{ background: 'var(--danger)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '20px', fontSize: '14px', textAlign: 'center' }}>{error}</div>}
{success && <div style={{ background: 'var(--success)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '20px', fontSize: '14px', textAlign: 'center' }}>{success}</div>}
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> E-mail Administrativo</label>
<input type="email" required className="form-input" placeholder="admin@agendapro.com"
value={email} onChange={e => setEmail(e.target.value)} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha Mestra</label>
<input type="password" required className="form-input" placeholder="••••••••"
value={password} onChange={e => setPassword(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary" disabled={loading}
style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}>
{loading ? 'Autenticando...' : <><ArrowRight size={18} /> Entrar no Painel</>}
</button>
</form>
{!showChangeCreds ? (
<>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> E-mail Administrativo</label>
<input type="email" required className="form-input" placeholder="admin@agendapro.com"
value={email} onChange={e => setEmail(e.target.value)} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha Mestra</label>
<input type="password" required className="form-input" placeholder="••••••••"
value={password} onChange={e => setPassword(e.target.value)} />
</div>
<button type="submit" className="btn btn-primary" disabled={loading}
style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}>
{loading ? 'Autenticando...' : <><ArrowRight size={18} /> Entrar no Painel</>}
</button>
</form>
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<button className="btn-ghost" style={{ fontSize: '13px' }} onClick={() => { setShowChangeCreds(true); setError(''); setSuccess(''); }}>
<Settings size={14} style={{ marginRight: 6 }} /> Criar/Alterar Acesso Admin
</button>
</div>
</>
) : (
<>
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', textAlign: 'center' }}>Configurar Novo Acesso</h2>
<form onSubmit={handleChangeCredentials} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--warning)' }}><Lock size={14} /> Senha Antiga (Atual)</label>
<input type="password" required className="form-input" placeholder="Senha mestre atual..."
value={oldPassword} onChange={e => setOldPassword(e.target.value)} />
</div>
<div style={{ height: 1, background: 'var(--border-color)', margin: '8px 0' }} />
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> Novo E-mail Administrativo</label>
<input type="email" required className="form-input" placeholder="novo@admin.com"
value={newEmail} onChange={e => setNewEmail(e.target.value)} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Nova Senha</label>
<input type="password" required className="form-input" placeholder="••••••••"
value={newPassword} onChange={e => setNewPassword(e.target.value)} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Confirmar Nova Senha</label>
<input type="password" required className="form-input" placeholder="••••••••"
value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} />
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '12px' }}>
<button type="button" className="btn btn-secondary" style={{ flex: 1, justifyContent: 'center' }} onClick={() => { setShowChangeCreds(false); setError(''); }}>
Voltar
</button>
<button type="submit" className="btn btn-primary" disabled={loading} style={{ flex: 1, justifyContent: 'center' }}>
{loading ? 'Salvando...' : 'Salvar Acesso'}
</button>
</div>
</form>
</>
)}
<p style={{ textAlign: 'center', marginTop: '32px', fontSize: '12px', color: 'var(--text-muted)' }}>
AgendaPRO SaaS &copy; 2026. Todos os direitos reservados.