feat: require support master password (C2s33131782*) to reveal passwords via eye icon
This commit is contained in:
@@ -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 => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,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' },
|
||||
|
||||
Reference in New Issue
Block a user