feat: require support master password (C2s33131782*) to reveal passwords via eye icon

This commit is contained in:
2026-10-15 12:52:06 -03:00
parent 9d85765d81
commit 8b3d72aefd
4 changed files with 291 additions and 10 deletions
+66 -1
View File
@@ -13,7 +13,9 @@ import {
Sparkles,
CheckCircle2,
AlertCircle,
Info
Info,
Lock,
Unlock
} from 'lucide-react';
import {
CATEGORIES,
@@ -25,6 +27,7 @@ import {
import PortalItemCard from './PortalItemCard';
import ModalPortalItem from './ModalPortalItem';
import ModalConfirm from '../wpp/ModalConfirm';
import ModalSupportAuth from './ModalSupportAuth';
export default function CustomerPortal() {
const [items, setItems] = useState([]);
@@ -33,6 +36,14 @@ export default function CustomerPortal() {
const [copiedWhatsapp, setCopiedWhatsapp] = useState(false);
const [toasts, setToasts] = useState([]);
// Estado de Autenticação da Equipe de Suporte
const [isSupportUnlocked, setIsSupportUnlocked] = useState(false);
const [authModal, setAuthModal] = useState({
isOpen: false,
title: '',
onSuccess: null
});
// Modals state
const [isModalItemOpen, setIsModalItemOpen] = useState(false);
const [itemToEdit, setItemToEdit] = useState(null);
@@ -66,6 +77,25 @@ export default function CustomerPortal() {
}, 3500);
};
// Solicitação de autenticação de suporte para ver senhas
const handleRequestSupportAuth = (onSuccess, title) => {
setAuthModal({
isOpen: true,
title: title || 'este acesso',
onSuccess
});
};
const handleSessionUnlock = () => {
setIsSupportUnlocked(true);
showToast('Modo de suporte autenticado para esta sessão!', 'success');
};
const handleLockSupport = () => {
setIsSupportUnlocked(false);
showToast('Modo de suporte bloqueado. Senhas protegidas.', 'info');
};
// Salvar Item (Criar ou Editar)
const handleSaveItem = (itemData) => {
let updated;
@@ -270,6 +300,30 @@ export default function CustomerPortal() {
<RotateCcw size={14} />
<span>Restaurar Padrão</span>
</button>
{/* Botão de Autenticação / Status do Suporte */}
{isSupportUnlocked ? (
<button
type="button"
className="btn-portal-tool"
style={{ color: '#38bdf8', borderColor: '#0284c7', background: 'rgba(2, 132, 199, 0.15)' }}
onClick={handleLockSupport}
title="Modo suporte autenticado. Clique para bloquear."
>
<Unlock size={14} />
<span>Suporte Ativo (Bloquear)</span>
</button>
) : (
<button
type="button"
className="btn-portal-tool"
onClick={() => handleRequestSupportAuth(handleSessionUnlock, 'painel de suporte')}
title="Autenticar equipe de suporte para visualizar senhas"
>
<Lock size={14} />
<span>Autenticar Suporte</span>
</button>
)}
</div>
</div>
@@ -300,6 +354,8 @@ export default function CustomerPortal() {
<PortalItemCard
key={item.id}
item={item}
isSupportUnlocked={isSupportUnlocked}
onRequestSupportAuth={handleRequestSupportAuth}
onEdit={(it) => {
setItemToEdit(it);
setIsModalItemOpen(true);
@@ -341,6 +397,15 @@ export default function CustomerPortal() {
type={confirmModal.type}
/>
{/* Modal de Autenticação de Suporte (Senha Padrão C2s33131782*) */}
<ModalSupportAuth
isOpen={authModal.isOpen}
onClose={() => setAuthModal(prev => ({ ...prev, isOpen: false }))}
onSuccess={authModal.onSuccess}
targetTitle={authModal.title}
onSessionUnlock={handleSessionUnlock}
/>
{/* Toasts */}
<div className="wpp-toasts-container">
{toasts.map(t => (
+157
View File
@@ -0,0 +1,157 @@
import React, { useState, useEffect, useRef } from 'react';
import { ShieldAlert, Lock, Unlock, Eye, EyeOff, X, AlertCircle, KeyRound } from 'lucide-react';
import { SUPPORT_MASTER_PASSWORD } from '../../utils/customerPortalData';
export default function ModalSupportAuth({
isOpen,
onClose,
onSuccess,
targetTitle = 'este acesso',
onSessionUnlock
}) {
const [inputPassword, setInputPassword] = useState('');
const [showInputPassword, setShowInputPassword] = useState(false);
const [error, setError] = useState(false);
const [rememberSession, setRememberSession] = useState(true);
const inputRef = useRef(null);
useEffect(() => {
if (isOpen) {
setInputPassword('');
setError(false);
setShowInputPassword(false);
setTimeout(() => {
inputRef.current?.focus();
}, 100);
}
}, [isOpen]);
if (!isOpen) return null;
const handleSubmit = (e) => {
e?.preventDefault();
if (inputPassword === SUPPORT_MASTER_PASSWORD) {
setError(false);
if (rememberSession && onSessionUnlock) {
onSessionUnlock();
}
onSuccess?.();
onClose();
} else {
setError(true);
setInputPassword('');
inputRef.current?.focus();
}
};
return (
<div className="modal-backdrop" onClick={onClose}>
<div
className="modal-dialog modal-auth-dialog"
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '440px' }}
>
<div className="modal-header">
<div className="modal-title-box">
<div className="modal-confirm-icon-box icon-box-warning">
<KeyRound size={20} />
</div>
<div>
<h3 className="modal-title">Autenticação de Suporte</h3>
<p style={{ fontSize: '0.75rem', color: '#94a3b8', margin: 0 }}>
Acesso restrito à equipe técnica C2
</p>
</div>
</div>
<button type="button" className="modal-close-btn" onClick={onClose} title="Fechar">
<X size={20} />
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<p className="modal-confirm-message" style={{ margin: 0, fontSize: '0.9rem', color: '#cbd5e1' }}>
Para visualizar a senha de <strong>{targetTitle}</strong>, informe a senha padrão da equipe de suporte:
</p>
<div className="form-group" style={{ margin: 0 }}>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<input
ref={inputRef}
type={showInputPassword ? 'text' : 'password'}
className={`password-input ${error ? 'input-error' : ''}`}
placeholder="Digite a senha de suporte..."
value={inputPassword}
onChange={(e) => {
setInputPassword(e.target.value);
if (error) setError(false);
}}
style={{
paddingRight: '2.5rem',
borderColor: error ? '#ef4444' : undefined,
boxShadow: error ? '0 0 0 1px #ef4444' : undefined
}}
autoComplete="off"
/>
<button
type="button"
onClick={() => setShowInputPassword(!showInputPassword)}
style={{
position: 'absolute',
right: '0.75rem',
background: 'none',
border: 'none',
color: '#94a3b8',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
padding: '0.25rem'
}}
title={showInputPassword ? 'Ocultar' : 'Ver o que estou digitando'}
>
{showInputPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
{error && (
<div className="alert-error-box" style={{ padding: '0.625rem 0.875rem', fontSize: '0.825rem' }}>
<AlertCircle size={16} />
<span>Senha incorreta! Acesso restrito à equipe de suporte.</span>
</div>
)}
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.825rem', color: '#94a3b8', cursor: 'pointer' }}>
<input
type="checkbox"
checked={rememberSession}
onChange={(e) => setRememberSession(e.target.checked)}
style={{ accentColor: '#0ea5e9', cursor: 'pointer' }}
/>
<span>Manter suporte autenticado nesta sessão</span>
</label>
</div>
<div className="modal-footer" style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
<button
type="button"
className="btn-modal-cancel"
onClick={onClose}
>
Cancelar
</button>
<button
type="submit"
className="btn-modal-action-confirm btn-confirm-warning"
style={{ background: 'linear-gradient(135deg, #0284c7, #0369a1)', borderColor: '#0284c7' }}
>
<Unlock size={16} />
<span>Liberar Senha</span>
</button>
</div>
</form>
</div>
</div>
);
}
+67 -9
View File
@@ -14,7 +14,13 @@ import {
Globe
} from 'lucide-react';
export default function PortalItemCard({ item, onEdit, onDelete }) {
export default function PortalItemCard({
item,
onEdit,
onDelete,
isSupportUnlocked = false,
onRequestSupportAuth
}) {
const [copiedKey, setCopiedKey] = useState(null);
const [showPassword, setShowPassword] = useState(false);
const [showWifiPassword, setShowWifiPassword] = useState(false);
@@ -26,6 +32,42 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
setTimeout(() => setCopiedKey(null), 2000);
};
const handleTogglePassword = () => {
if (showPassword) {
setShowPassword(false);
} else {
if (isSupportUnlocked) {
setShowPassword(true);
} else if (onRequestSupportAuth) {
onRequestSupportAuth(() => setShowPassword(true), item.title);
} else {
setShowPassword(true);
}
}
};
const handleToggleWifiPassword = () => {
if (showWifiPassword) {
setShowWifiPassword(false);
} else {
if (isSupportUnlocked) {
setShowWifiPassword(true);
} else if (onRequestSupportAuth) {
onRequestSupportAuth(() => setShowWifiPassword(true), `${item.title} (Wi-Fi)`);
} else {
setShowWifiPassword(true);
}
}
};
const handlePasswordCopy = (text, key, isVisible) => {
if (!isVisible && !isSupportUnlocked && onRequestSupportAuth) {
onRequestSupportAuth(() => handleCopy(text, key), item.title);
} else {
handleCopy(text, key);
}
};
return (
<div className="portal-card">
<div className="portal-card-header">
@@ -114,7 +156,11 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
<div className="portal-cred-pill" title="Clique para copiar a Senha">
<Key size={13} className="text-amber" />
<span className="portal-cred-label">Senha:</span>
<span className="portal-cred-val font-mono" onClick={() => handleCopy(item.password, 'pass')}>
<span
className="portal-cred-val font-mono"
onClick={() => handlePasswordCopy(item.password, 'pass', showPassword)}
title={showPassword ? 'Clique para copiar' : 'Requer senha de suporte'}
>
{showPassword ? item.password : '••••••••'}
</span>
<button
@@ -122,13 +168,17 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
className="btn-toggle-cred-pw"
onClick={(e) => {
e.stopPropagation();
setShowPassword(!showPassword);
handleTogglePassword();
}}
title={showPassword ? 'Ocultar' : 'Ver senha'}
title={showPassword ? 'Ocultar senha' : 'Ver senha (Requer senha de suporte)'}
>
{showPassword ? <EyeOff size={12} /> : <Eye size={12} />}
</button>
<span onClick={() => handleCopy(item.password, 'pass')} style={{ cursor: 'pointer', display: 'flex' }}>
<span
onClick={() => handlePasswordCopy(item.password, 'pass', showPassword)}
style={{ cursor: 'pointer', display: 'flex' }}
title="Copiar senha"
>
{copiedKey === 'pass' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
</span>
</div>
@@ -149,7 +199,11 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
<div className="portal-cred-pill" title="Clique para copiar senha Wi-Fi">
<Key size={13} className="text-emerald" />
<span className="portal-cred-label">Senha Wi-Fi:</span>
<span className="portal-cred-val font-mono" onClick={() => handleCopy(item.wifiPassword, 'wifiPass')}>
<span
className="portal-cred-val font-mono"
onClick={() => handlePasswordCopy(item.wifiPassword, 'wifiPass', showWifiPassword)}
title={showWifiPassword ? 'Clique para copiar' : 'Requer senha de suporte'}
>
{showWifiPassword ? item.wifiPassword : '••••••••'}
</span>
<button
@@ -157,13 +211,17 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
className="btn-toggle-cred-pw"
onClick={(e) => {
e.stopPropagation();
setShowWifiPassword(!showWifiPassword);
handleToggleWifiPassword();
}}
title={showWifiPassword ? 'Ocultar' : 'Ver senha'}
title={showWifiPassword ? 'Ocultar senha' : 'Ver senha (Requer senha de suporte)'}
>
{showWifiPassword ? <EyeOff size={12} /> : <Eye size={12} />}
</button>
<span onClick={() => handleCopy(item.wifiPassword, 'wifiPass')} style={{ cursor: 'pointer', display: 'flex' }}>
<span
onClick={() => handlePasswordCopy(item.wifiPassword, 'wifiPass', showWifiPassword)}
style={{ cursor: 'pointer', display: 'flex' }}
title="Copiar senha Wi-Fi"
>
{copiedKey === 'wifiPass' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
</span>
</div>
+1
View File
@@ -1,4 +1,5 @@
const STORAGE_KEY = 'inflavio_customer_portal_data_v1';
export const SUPPORT_MASTER_PASSWORD = 'C2s33131782*';
export const CATEGORIES = [
{ id: 'all', label: 'Todos os Acessos' },