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 { NextResponse } from 'next/server';
|
||||||
|
import { query } from '@/lib/db';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
|
|
@ -6,26 +8,49 @@ export async function POST(req: Request) {
|
||||||
const { email, password } = await req.json();
|
const { email, password } = await req.json();
|
||||||
if (!email || !password) return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
|
if (!email || !password) return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
|
||||||
|
|
||||||
// Hardcoded Admin Credentials for SaaS owner
|
let isValid = false;
|
||||||
// In production, you'd use ENV variables like process.env.ADMIN_EMAIL
|
let adminName = 'Super Admin';
|
||||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@agendapro.com';
|
let adminEmail = email;
|
||||||
const ADMIN_PASS = process.env.ADMIN_PASSWORD || 'admin123';
|
|
||||||
|
|
||||||
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 });
|
return NextResponse.json({ error: 'Credenciais inválidas' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = process.env.JWT_SECRET || 'fallback_secret_for_development';
|
const secret = process.env.JWT_SECRET || 'fallback_secret_for_development';
|
||||||
const token = jwt.sign({
|
const token = jwt.sign({
|
||||||
role: 'superadmin',
|
role: 'superadmin',
|
||||||
email: ADMIN_EMAIL
|
email: adminEmail
|
||||||
}, secret, { expiresIn: '7d' });
|
}, secret, { expiresIn: '7d' });
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
token,
|
token,
|
||||||
user: {
|
user: {
|
||||||
name: 'Super Admin',
|
name: adminName,
|
||||||
email: ADMIN_EMAIL,
|
email: adminEmail,
|
||||||
role: 'superadmin'
|
role: 'superadmin'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
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';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function AdminLoginPage() {
|
export default function AdminLoginPage() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = 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 [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
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 (
|
return (
|
||||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-primary)' }}>
|
<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' }}>
|
<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>
|
</div>
|
||||||
|
|
||||||
{error && <div style={{ background: 'var(--danger)', color: 'white', padding: '12px', borderRadius: 'var(--radius-md)', marginBottom: '20px', fontSize: '14px', textAlign: 'center' }}>{error}</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' }}>
|
{!showChangeCreds ? (
|
||||||
<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>
|
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||||
<input type="email" required className="form-input" placeholder="admin@agendapro.com"
|
<div className="form-group" style={{ margin: 0 }}>
|
||||||
value={email} onChange={e => setEmail(e.target.value)} />
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Mail size={14} /> E-mail Administrativo</label>
|
||||||
</div>
|
<input type="email" required className="form-input" placeholder="admin@agendapro.com"
|
||||||
<div className="form-group" style={{ margin: 0 }}>
|
value={email} onChange={e => setEmail(e.target.value)} />
|
||||||
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha Mestra</label>
|
</div>
|
||||||
<input type="password" required className="form-input" placeholder="••••••••"
|
<div className="form-group" style={{ margin: 0 }}>
|
||||||
value={password} onChange={e => setPassword(e.target.value)} />
|
<label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Lock size={14} /> Senha Mestra</label>
|
||||||
</div>
|
<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}
|
<button type="submit" className="btn btn-primary" disabled={loading}
|
||||||
style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}>
|
style={{ width: '100%', padding: '12px', fontSize: '16px', justifyContent: 'center', marginTop: '12px' }}>
|
||||||
{loading ? 'Autenticando...' : <><ArrowRight size={18} /> Entrar no Painel</>}
|
{loading ? 'Autenticando...' : <><ArrowRight size={18} /> Entrar no Painel</>}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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)' }}>
|
<p style={{ textAlign: 'center', marginTop: '32px', fontSize: '12px', color: 'var(--text-muted)' }}>
|
||||||
AgendaPRO SaaS © 2026. Todos os direitos reservados.
|
AgendaPRO SaaS © 2026. Todos os direitos reservados.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue