feat: adicionar interface e rota para alterar credenciais de admin
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m37s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m37s
Details
This commit is contained in:
parent
9d13e6626d
commit
baaa36f0a2
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{!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>
|
||||
<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 © 2026. Todos os direitos reservados.
|
||||
|
|
|
|||
Loading…
Reference in New Issue