Implementa fluxo de Esqueci a Senha duplo com e-mail SMTP e notificacoes WhatsApp via Evolution API
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s Details

This commit is contained in:
Sidney Gomes 2026-07-23 22:05:10 -03:00
parent 16cbca320e
commit 4d130b5b5d
6 changed files with 490 additions and 6 deletions

21
package-lock.json generated
View File

@ -21,6 +21,7 @@
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"multer": "^1.4.5-lts.1",
"nodemailer": "^9.0.3",
"prisma": "^6.4.1",
"react": "^19.0.1",
"react-dom": "^19.0.1",
@ -33,6 +34,7 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^1.4.12",
"@types/node": "^22.14.0",
"@types/nodemailer": "^8.0.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.21",
@ -1990,6 +1992,16 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/nodemailer": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz",
"integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.15.1",
"dev": true,
@ -3906,6 +3918,15 @@
"node": ">=18"
}
},
"node_modules/nodemailer": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nypm": {
"version": "0.6.8",
"license": "MIT",

View File

@ -24,6 +24,7 @@
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"multer": "^1.4.5-lts.1",
"nodemailer": "^9.0.3",
"prisma": "^6.4.1",
"react": "^19.0.1",
"react-dom": "^19.0.1",
@ -36,6 +37,7 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^1.4.12",
"@types/node": "^22.14.0",
"@types/nodemailer": "^8.0.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.21",

View File

@ -58,8 +58,9 @@ model User {
birthDate String? @map("birth_date")
avatarUrl String? @map("avatar_url") @db.Text
trialEndsAt DateTime? @map("trial_ends_at")
resetPasswordToken String? @map("reset_password_token") @db.Text
resetPasswordExpires DateTime? @map("reset_password_expires")
createdAt DateTime @default(now()) @map("created_at")
notes Note[]
progress Progress[]
payments Payment[]
@ -267,6 +268,13 @@ model SystemSettings {
cnpj String @default("") @map("cnpj")
address String @default("") @map("address")
smtpHost String? @map("smtp_host")
smtpPort Int? @default(587) @map("smtp_port")
smtpUser String? @map("smtp_user")
smtpPass String? @map("smtp_pass")
smtpFrom String? @default("no-reply@microtecinformaticacurso.com.br") @map("smtp_from")
smtpSecure Boolean @default(false) @map("smtp_secure")
@@map("system_settings")
}

156
server.ts
View File

@ -6,6 +6,8 @@ import multer from 'multer';
import axios from 'axios';
import { execSync } from 'child_process';
import { S3Client, PutObjectCommand, CreateBucketCommand, HeadBucketCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3';
import crypto from 'crypto';
import nodemailer from 'nodemailer';
import { prisma } from './src/db/prisma';
import { Prisma } from '@prisma/client';
import { AsaasService } from './src/services/asaas';
@ -75,8 +77,27 @@ async function initMinio() {
}
}
// Start initialization async, don't block server start
// Start initialization async, don't block server start
initMinio();
// --- SMTP EMAILS CONFIGURATION ---
async function getTransporter() {
const settings = await prisma.systemSettings.findFirst();
if (!settings || !settings.smtpHost) {
throw new Error('As credenciais SMTP não estão configuradas.');
}
return nodemailer.createTransport({
host: settings.smtpHost,
port: settings.smtpPort || 587,
secure: settings.smtpSecure,
auth: {
user: settings.smtpUser,
pass: settings.smtpPass,
},
});
}
// --- JWT AUTH MIDDLEWARE ---
interface AuthenticatedRequest extends Request {
@ -217,6 +238,103 @@ app.post('/api/auth/login', async (req: Request, res: Response) => {
}
});
// 2.5 Auth: Forgot Password
app.post('/api/auth/forgot-password', async (req: Request, res: Response) => {
const { identifier, channel } = req.body;
if (!identifier || !channel) {
res.status(400).json({ error: 'Informe o identificador e o canal (email ou whatsapp).' });
return;
}
try {
const user = await prisma.user.findFirst({
where: channel === 'whatsapp' ? { whatsapp: identifier } : { email: identifier.toLowerCase() }
});
if (!user) {
res.json({ success: true, message: 'Se o usuário existir, o link foi enviado.' });
return;
}
const token = crypto.randomBytes(32).toString('hex');
const expires = new Date(Date.now() + 3600000); // 1 hour
await prisma.user.update({
where: { id: user.id },
data: { resetPasswordToken: token, resetPasswordExpires: expires }
});
const resetUrl = `https://estudo.microtecinformaticacurso.com.br/reset-password?token=${token}`;
if (channel === 'whatsapp') {
const text = `*Recuperação de Senha - MicrotecFlix*\n\nVocê solicitou a recuperação da sua senha.\nClique no link abaixo para criar uma nova senha:\n\n${resetUrl}\n\n_Se você não solicitou isso, ignore esta mensagem._`;
await sendWhatsappNotification(user.whatsapp || identifier, text);
} else {
const transporter = await getTransporter();
const settings = await prisma.systemSettings.findFirst();
await transporter.sendMail({
from: `"${settings?.brandName || 'Microtec'}Flix" <${settings?.smtpFrom || 'no-reply@microtecinformaticacurso.com.br'}>`,
to: user.email,
subject: 'Recuperação de Senha',
html: `
<div style="background-color: #141414; color: #fff; padding: 40px; font-family: sans-serif; text-align: center;">
<h1 style="color: #E50914;">${settings?.brandName || 'MICROTEC'}FLIX</h1>
<h2>Recuperação de Senha</h2>
<p>Você solicitou a recuperação da sua senha. Clique no botão abaixo para redefini-la (válido por 1 hora).</p>
<a href="${resetUrl}" style="display: inline-block; padding: 15px 30px; margin: 20px 0; background-color: #E50914; color: #fff; text-decoration: none; font-weight: bold; border-radius: 4px;">Redefinir Senha</a>
<p style="color: #666; font-size: 12px;">Se você não solicitou, apenas ignore este e-mail.</p>
</div>
`
});
}
res.json({ success: true, message: 'Link de recuperação enviado com sucesso.' });
} catch (error: any) {
console.error('Forgot password error:', error);
res.status(500).json({ error: error.message || 'Erro ao enviar link de recuperação.' });
}
});
// 2.6 Auth: Reset Password
app.post('/api/auth/reset-password', async (req: Request, res: Response) => {
const { token, newPassword } = req.body;
if (!token || !newPassword) {
res.status(400).json({ error: 'Token e nova senha são obrigatórios.' });
return;
}
try {
const user = await prisma.user.findFirst({
where: {
resetPasswordToken: token,
resetPasswordExpires: { gte: new Date() }
}
});
if (!user) {
res.status(400).json({ error: 'Token inválido ou expirado.' });
return;
}
const hashedPassword = bcrypt.hashSync(newPassword, 10);
await prisma.user.update({
where: { id: user.id },
data: {
password: hashedPassword,
resetPasswordToken: null,
resetPasswordExpires: null
}
});
res.json({ success: true, message: 'Senha redefinida com sucesso!' });
} catch (error) {
console.error('Reset password error:', error);
res.status(500).json({ error: 'Erro ao redefinir senha.' });
}
});
// 3. Auth: Me
app.get('/api/auth/me', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
if (!req.user) {
@ -1062,7 +1180,13 @@ app.get('/api/admin/settings', authenticateToken, requireAdmin, async (req: Auth
brandName: settings?.brandName || 'MICROTEC',
brandSlogan: settings?.brandSlogan || 'Acelere seus estudos em informática e pacote Office no estilo Netflix.',
cnpj: settings?.cnpj || '',
address: settings?.address || ''
address: settings?.address || '',
smtpHost: settings?.smtpHost || '',
smtpPort: settings?.smtpPort || 587,
smtpUser: settings?.smtpUser || '',
smtpPass: settings?.smtpPass || '',
smtpFrom: settings?.smtpFrom || 'no-reply@microtecinformaticacurso.com.br',
smtpSecure: settings?.smtpSecure || false
});
});
@ -1095,6 +1219,36 @@ app.post('/api/admin/test-asaas', authenticateToken, requireAdmin, async (req: A
}
});
app.post('/api/admin/test-smtp', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
try {
const { smtpHost, smtpPort, smtpSecure, smtpUser, smtpPass, smtpFrom } = req.body;
const transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
auth: {
user: smtpUser,
pass: smtpPass
}
});
await transporter.verify();
// Envia um email de teste para o próprio remetente (ou admin)
await transporter.sendMail({
from: smtpFrom,
to: req.user!.email,
subject: 'Teste de Conexão SMTP - MicrotecFlix',
text: 'Se você está recebendo este e-mail, sua configuração SMTP na MicrotecFlix foi realizada com sucesso!'
});
res.json({ success: true, message: 'Conexão estabelecida e e-mail de teste enviado com sucesso!' });
} catch (err: any) {
res.status(500).json({ success: false, message: err.message || 'Erro ao testar conexão SMTP' });
}
});
// --- EVOLUTION API WHATSAPP ---
app.get('/api/admin/whatsapp/qrcode', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
try {

View File

@ -30,6 +30,8 @@ export default function App() {
// Authentication forms
const [isRegister, setIsRegister] = useState(false);
const [isForgotPassword, setIsForgotPassword] = useState(false);
const [resetToken, setResetToken] = useState<string | null>(null);
const [loginEmail, setLoginEmail] = useState('');
const [loginPassword, setLoginPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
@ -50,6 +52,13 @@ export default function App() {
const [settings, setSettings] = useState<{ brandName?: string; brandSlogan?: string; cnpj?: string; address?: string } | null>(null);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const tokenParam = urlParams.get('token');
if (window.location.pathname === '/reset-password' && tokenParam) {
setResetToken(tokenParam);
setIsForgotPassword(true);
}
fetch('/api/settings')
.then(res => res.json())
.then(data => {
@ -111,6 +120,55 @@ export default function App() {
}
};
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault();
setAuthError(null);
setAuthSuccess(null);
setAuthLoading(true);
try {
const res = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: loginEmail, channel: loginEmail.includes('@') ? 'email' : 'whatsapp' })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao solicitar recuperação.');
setAuthSuccess(data.message || 'Link de recuperação enviado.');
} catch (err: any) {
setAuthError(err.message);
} finally {
setAuthLoading(false);
}
};
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault();
if (regPassword !== regPassword.trim() || regPassword.length < 6) {
setAuthError('A senha deve ter no mínimo 6 caracteres.');
return;
}
setAuthError(null);
setAuthSuccess(null);
setAuthLoading(true);
try {
const res = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: resetToken, newPassword: regPassword })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Erro ao redefinir senha.');
setAuthSuccess(data.message || 'Senha redefinida com sucesso! Redirecionando...');
setTimeout(() => {
window.location.href = '/';
}, 3000);
} catch (err: any) {
setAuthError(err.message);
} finally {
setAuthLoading(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setAuthError(null);
@ -345,14 +403,86 @@ export default function App() {
</p>
</div>
{/* Error indicators */}
{authError && (
<div className="bg-red-950/40 border border-red-900/50 text-red-400 p-3 rounded-lg text-xs leading-relaxed text-center">
{authError}
</div>
)}
{authSuccess && (
<div className="bg-emerald-950/40 border border-emerald-900/50 text-emerald-400 p-3 rounded-lg text-xs leading-relaxed text-center">
{authSuccess}
</div>
)}
{isRegister ? (
{resetToken ? (
/* RESET PASSWORD FORM */
<form onSubmit={handleResetPassword} className="space-y-4">
<div className="space-y-1">
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">Nova Senha</label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-4 h-4 text-zinc-500" />
<input
type={showPassword ? "text" : "password"}
required
value={regPassword}
onChange={(e) => setRegPassword(e.target.value)}
placeholder="Mínimo 6 caracteres"
className="w-full glass-input rounded-lg p-3 pl-10 pr-10 text-xs text-white focus:outline-none"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-zinc-500 hover:text-white transition-colors"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<button
type="submit"
disabled={authLoading}
className="w-full glass-btn-primary text-white text-xs font-bold py-3 px-4 rounded-lg flex items-center justify-center space-x-1.5 cursor-pointer disabled:opacity-50"
>
<span>{authLoading ? 'Redefinindo...' : 'Salvar Nova Senha'}</span>
</button>
</form>
) : isForgotPassword ? (
/* FORGOT PASSWORD FORM */
<form onSubmit={handleForgotPassword} className="space-y-4">
<div className="space-y-1">
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">E-mail ou WhatsApp (com DDI ex: 5511999999999)</label>
<div className="relative">
<Mail className="absolute left-3 top-3 w-4 h-4 text-zinc-500" />
<input
type="text"
required
value={loginEmail}
onChange={(e) => setLoginEmail(e.target.value)}
placeholder="email@exemplo.com ou 5511999999999"
className="w-full glass-input rounded-lg p-3 pl-10 text-xs text-white focus:outline-none"
/>
</div>
</div>
<button
type="submit"
disabled={authLoading}
className="w-full glass-btn-primary text-white text-xs font-bold py-3 px-4 rounded-lg flex items-center justify-center space-x-1.5 cursor-pointer disabled:opacity-50"
>
<span>{authLoading ? 'Enviando...' : 'Receber Link de Recuperação'}</span>
</button>
<p className="text-center text-xs text-zinc-500">
Lembrou a senha?{' '}
<button
type="button"
onClick={() => { setIsForgotPassword(false); setAuthError(null); setAuthSuccess(null); }}
className="text-white hover:underline font-bold"
>
Fazer Login
</button>
</p>
</form>
) : isRegister ? (
/* REGISTER FORM */
<form onSubmit={handleRegister} className="space-y-4">
<div className="space-y-1">
@ -495,6 +625,16 @@ export default function App() {
</div>
</div>
<div className="flex justify-end">
<button
type="button"
onClick={() => { setIsForgotPassword(true); setAuthError(null); setAuthSuccess(null); }}
className="text-[10px] text-zinc-400 hover:text-white transition-colors"
>
Esqueci minha senha
</button>
</div>
<button
type="submit"
disabled={authLoading}

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import {
TrendingUp, Users, AlertTriangle, XCircle, Search,
Settings, CheckCircle2, RefreshCw, Link as LinkIcon, ShieldCheck, HelpCircle, Eye, DollarSign,
Shield, Layers, LogOut, Menu, X, GraduationCap
Shield, Layers, LogOut, Menu, X, GraduationCap, Mail
} from 'lucide-react';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Legend, Cell, PieChart, Pie } from 'recharts';
import { SubscriptionStatus } from '../types';
@ -75,7 +75,7 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
const [expandedStudentId, setExpandedStudentId] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState<'dashboard' | 'students' | 'courses' | 'plans' | 'payments' | 'asaas_settings' | 'whatsapp_settings' | 'webhooks_logs' | 'saas_settings'>('dashboard');
const [activeTab, setActiveTab] = useState<'dashboard' | 'students' | 'courses' | 'plans' | 'payments' | 'asaas_settings' | 'whatsapp_settings' | 'webhooks_logs' | 'saas_settings' | 'smtp_settings'>('dashboard');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [actionLoading, setActionLoading] = useState<string | null>(null);
@ -85,6 +85,7 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
const [isSavingSettings, setIsSavingSettings] = useState(false);
const [showSaveSuccessModal, setShowSaveSuccessModal] = useState(false);
const [asaasTestResult, setAsaasTestResult] = useState<{ loading: boolean; success: boolean | null; message: string } | null>(null);
const [smtpTestResult, setSmtpTestResult] = useState<{ loading: boolean; success: boolean | null; message: string } | null>(null);
useEffect(() => {
fetchDashboardData();
@ -165,6 +166,39 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
}
};
const handleTestSmtpConnection = async () => {
setSmtpTestResult({ loading: true, success: null, message: 'Testando conexão SMTP...' });
try {
const res = await fetch('/api/admin/test-smtp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
smtpHost: settings?.smtpHost,
smtpPort: parseInt(settings?.smtpPort || '587'),
smtpUser: settings?.smtpUser,
smtpPass: settings?.smtpPass,
smtpSecure: settings?.smtpSecure,
smtpFrom: settings?.smtpFrom
})
});
const data = await res.json();
setSmtpTestResult({
loading: false,
success: data.success,
message: data.message || (data.success ? 'Conexão SMTP estabelecida e e-mail enviado com sucesso!' : 'Falha na conexão SMTP.')
});
} catch (err: any) {
setSmtpTestResult({
loading: false,
success: false,
message: 'Falha de comunicação com o servidor ao testar SMTP.'
});
}
};
const fetchCourses = async () => {
try {
const res = await fetch('/api/courses', {
@ -432,6 +466,13 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
<ShieldCheck size={16} />
<span>Configuração do SaaS</span>
</button>
<button
onClick={() => { setActiveTab('smtp_settings'); setIsMobileMenuOpen(false); }}
className={`w-full flex items-center space-x-2.5 px-3 py-2 rounded-lg text-xs font-bold transition-all text-left ${activeTab === 'smtp_settings' ? 'bg-[#E50914]/10 text-white border-l-2 border-[#E50914]' : 'text-zinc-400 hover:text-white hover:bg-white/5'}`}
>
<Mail size={16} />
<span>Configurações SMTP</span>
</button>
</div>
</nav>
</div>
@ -1281,6 +1322,124 @@ export default function AdminDashboard({ token, adminUser, onLogout }: AdminDash
</form>
</div>
)}
{/* TAB 10: SMTP CONFIGURATION */}
{activeTab === 'smtp_settings' && (
<div className="glass-card rounded-xl border border-white/10 p-6 shadow-2xl space-y-6">
<div className="flex items-center gap-3 border-b border-white/5 pb-4">
<Mail className="text-[#E50914]" size={24} />
<div>
<h2 className="text-xl font-bold text-white font-display">Configurações de Envio de E-mail (SMTP)</h2>
<p className="text-xs text-zinc-500">Credenciais para envio de e-mails transacionais (ex: Recuperação de Senha).</p>
</div>
</div>
<form onSubmit={handleSaveSettings} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-white/5 p-6 rounded-xl border border-white/5">
<div className="space-y-2">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Servidor SMTP (Host)</label>
<input
type="text"
value={settings.smtpHost || ''}
onChange={(e) => setSettings({...settings, smtpHost: e.target.value})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-[#E50914] transition-colors"
placeholder="Ex: smtp.hostinger.com"
/>
</div>
<div className="space-y-2">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Porta</label>
<input
type="number"
value={settings.smtpPort || ''}
onChange={(e) => setSettings({...settings, smtpPort: parseInt(e.target.value) || 587})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-[#E50914] transition-colors"
placeholder="Ex: 465 ou 587"
/>
</div>
<div className="space-y-2">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Usuário SMTP</label>
<input
type="text"
value={settings.smtpUser || ''}
onChange={(e) => setSettings({...settings, smtpUser: e.target.value})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-[#E50914] transition-colors"
placeholder="Ex: contato@seudominio.com.br"
/>
</div>
<div className="space-y-2">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">Senha SMTP</label>
<input
type="password"
value={settings.smtpPass || ''}
onChange={(e) => setSettings({...settings, smtpPass: e.target.value})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-[#E50914] transition-colors"
placeholder="**************"
/>
</div>
<div className="space-y-2">
<label className="text-xs text-zinc-400 font-bold uppercase tracking-wider">E-mail do Remetente (From)</label>
<input
type="text"
value={settings.smtpFrom || ''}
onChange={(e) => setSettings({...settings, smtpFrom: e.target.value})}
className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-[#E50914] transition-colors"
placeholder="Ex: no-reply@seudominio.com.br"
/>
</div>
<div className="space-y-2 flex flex-col justify-center">
<label className="flex items-center space-x-3 cursor-pointer mt-4">
<input
type="checkbox"
checked={settings.smtpSecure || false}
onChange={(e) => setSettings({...settings, smtpSecure: e.target.checked})}
className="form-checkbox h-5 w-5 text-[#E50914] rounded border-white/10 bg-black/40 focus:ring-[#E50914] focus:ring-offset-black"
/>
<span className="text-sm font-bold text-white">Usar Conexão Segura (SSL/TLS)</span>
</label>
<p className="text-[10px] text-zinc-500 mt-1 pl-8">Marque se a porta for 465. Para 587 (STARTTLS), normalmente fica desmarcado.</p>
</div>
</div>
{/* Test Results */}
{smtpTestResult && (
<div className={`p-4 rounded-xl text-xs font-bold ${smtpTestResult.loading ? 'bg-zinc-800 text-zinc-400' : smtpTestResult.success ? 'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20' : 'bg-red-500/10 text-red-500 border border-red-500/20'}`}>
{smtpTestResult.message}
</div>
)}
<div className="flex justify-end gap-3 mt-6">
<button
type="button"
onClick={handleTestSmtpConnection}
disabled={smtpTestResult?.loading}
className="glass-btn text-white font-bold py-3.5 px-6 rounded-xl border border-white/10 hover:bg-white/5 transition-all text-xs flex items-center gap-2 cursor-pointer disabled:opacity-50"
>
{smtpTestResult?.loading ? <RefreshCw className="animate-spin" size={16} /> : <Mail size={16} />}
<span>Enviar E-mail de Teste</span>
</button>
<button
type="submit"
disabled={isSavingSettings}
className="glass-btn-primary text-white font-extrabold py-3.5 px-8 rounded-xl shadow-lg hover:scale-105 transition-all flex items-center gap-2 cursor-pointer disabled:opacity-50"
>
{isSavingSettings ? (
<RefreshCw className="animate-spin" size={18} />
) : (
<CheckCircle2 size={18} />
)}
<span>{isSavingSettings ? 'Salvando...' : 'Salvar Configurações SMTP'}</span>
</button>
</div>
</form>
</div>
)}
</>
)}
</div>