From baaa36f0a2a673e7938ac02bbab0a15fdd305343 Mon Sep 17 00:00:00 2001 From: Sidney Date: Sun, 7 Jun 2026 20:38:41 -0300 Subject: [PATCH] feat: adicionar interface e rota para alterar credenciais de admin --- .../app/api/auth/change-credentials/route.ts | 59 ++++++++ admin/src/app/api/auth/login/route.ts | 41 ++++-- admin/src/app/login/page.tsx | 132 +++++++++++++++--- 3 files changed, 206 insertions(+), 26 deletions(-) create mode 100644 admin/src/app/api/auth/change-credentials/route.ts diff --git a/admin/src/app/api/auth/change-credentials/route.ts b/admin/src/app/api/auth/change-credentials/route.ts new file mode 100644 index 0000000..eb33516 --- /dev/null +++ b/admin/src/app/api/auth/change-credentials/route.ts @@ -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 }); + } +} diff --git a/admin/src/app/api/auth/login/route.ts b/admin/src/app/api/auth/login/route.ts index 02e5654..db02523 100644 --- a/admin/src/app/api/auth/login/route.ts +++ b/admin/src/app/api/auth/login/route.ts @@ -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' } }); diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx index 2a209c4..2284bce 100644 --- a/admin/src/app/login/page.tsx +++ b/admin/src/app/login/page.tsx @@ -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 (
@@ -55,24 +105,70 @@ export default function AdminLoginPage() {
{error &&
{error}
} + {success &&
{success}
} -
-
- - setEmail(e.target.value)} /> -
-
- - setPassword(e.target.value)} /> -
- - -
+ {!showChangeCreds ? ( + <> +
+
+ + setEmail(e.target.value)} /> +
+
+ + setPassword(e.target.value)} /> +
+ + +
+
+ +
+ + ) : ( + <> +

Configurar Novo Acesso

+
+
+ + setOldPassword(e.target.value)} /> +
+
+
+ + setNewEmail(e.target.value)} /> +
+
+ + setNewPassword(e.target.value)} /> +
+
+ + setConfirmPassword(e.target.value)} /> +
+ +
+ + +
+ + + )}

AgendaPRO SaaS © 2026. Todos os direitos reservados.