feat: require support master password (C2s33131782*) to reveal passwords via eye icon
This commit is contained in:
@@ -13,7 +13,9 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Info
|
Info,
|
||||||
|
Lock,
|
||||||
|
Unlock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
CATEGORIES,
|
CATEGORIES,
|
||||||
@@ -25,6 +27,7 @@ import {
|
|||||||
import PortalItemCard from './PortalItemCard';
|
import PortalItemCard from './PortalItemCard';
|
||||||
import ModalPortalItem from './ModalPortalItem';
|
import ModalPortalItem from './ModalPortalItem';
|
||||||
import ModalConfirm from '../wpp/ModalConfirm';
|
import ModalConfirm from '../wpp/ModalConfirm';
|
||||||
|
import ModalSupportAuth from './ModalSupportAuth';
|
||||||
|
|
||||||
export default function CustomerPortal() {
|
export default function CustomerPortal() {
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
@@ -33,6 +36,14 @@ export default function CustomerPortal() {
|
|||||||
const [copiedWhatsapp, setCopiedWhatsapp] = useState(false);
|
const [copiedWhatsapp, setCopiedWhatsapp] = useState(false);
|
||||||
const [toasts, setToasts] = useState([]);
|
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
|
// Modals state
|
||||||
const [isModalItemOpen, setIsModalItemOpen] = useState(false);
|
const [isModalItemOpen, setIsModalItemOpen] = useState(false);
|
||||||
const [itemToEdit, setItemToEdit] = useState(null);
|
const [itemToEdit, setItemToEdit] = useState(null);
|
||||||
@@ -66,6 +77,25 @@ export default function CustomerPortal() {
|
|||||||
}, 3500);
|
}, 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)
|
// Salvar Item (Criar ou Editar)
|
||||||
const handleSaveItem = (itemData) => {
|
const handleSaveItem = (itemData) => {
|
||||||
let updated;
|
let updated;
|
||||||
@@ -270,6 +300,30 @@ export default function CustomerPortal() {
|
|||||||
<RotateCcw size={14} />
|
<RotateCcw size={14} />
|
||||||
<span>Restaurar Padrão</span>
|
<span>Restaurar Padrão</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -300,6 +354,8 @@ export default function CustomerPortal() {
|
|||||||
<PortalItemCard
|
<PortalItemCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
|
isSupportUnlocked={isSupportUnlocked}
|
||||||
|
onRequestSupportAuth={handleRequestSupportAuth}
|
||||||
onEdit={(it) => {
|
onEdit={(it) => {
|
||||||
setItemToEdit(it);
|
setItemToEdit(it);
|
||||||
setIsModalItemOpen(true);
|
setIsModalItemOpen(true);
|
||||||
@@ -341,6 +397,15 @@ export default function CustomerPortal() {
|
|||||||
type={confirmModal.type}
|
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 */}
|
{/* Toasts */}
|
||||||
<div className="wpp-toasts-container">
|
<div className="wpp-toasts-container">
|
||||||
{toasts.map(t => (
|
{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
|
Globe
|
||||||
} from 'lucide-react';
|
} 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 [copiedKey, setCopiedKey] = useState(null);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [showWifiPassword, setShowWifiPassword] = useState(false);
|
const [showWifiPassword, setShowWifiPassword] = useState(false);
|
||||||
@@ -26,6 +32,42 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
|
|||||||
setTimeout(() => setCopiedKey(null), 2000);
|
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 (
|
return (
|
||||||
<div className="portal-card">
|
<div className="portal-card">
|
||||||
<div className="portal-card-header">
|
<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">
|
<div className="portal-cred-pill" title="Clique para copiar a Senha">
|
||||||
<Key size={13} className="text-amber" />
|
<Key size={13} className="text-amber" />
|
||||||
<span className="portal-cred-label">Senha:</span>
|
<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 : '••••••••'}
|
{showPassword ? item.password : '••••••••'}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -122,13 +168,17 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
|
|||||||
className="btn-toggle-cred-pw"
|
className="btn-toggle-cred-pw"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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} />}
|
{showPassword ? <EyeOff size={12} /> : <Eye size={12} />}
|
||||||
</button>
|
</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} />}
|
{copiedKey === 'pass' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +199,11 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
|
|||||||
<div className="portal-cred-pill" title="Clique para copiar senha Wi-Fi">
|
<div className="portal-cred-pill" title="Clique para copiar senha Wi-Fi">
|
||||||
<Key size={13} className="text-emerald" />
|
<Key size={13} className="text-emerald" />
|
||||||
<span className="portal-cred-label">Senha Wi-Fi:</span>
|
<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 : '••••••••'}
|
{showWifiPassword ? item.wifiPassword : '••••••••'}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -157,13 +211,17 @@ export default function PortalItemCard({ item, onEdit, onDelete }) {
|
|||||||
className="btn-toggle-cred-pw"
|
className="btn-toggle-cred-pw"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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} />}
|
{showWifiPassword ? <EyeOff size={12} /> : <Eye size={12} />}
|
||||||
</button>
|
</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} />}
|
{copiedKey === 'wifiPass' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const STORAGE_KEY = 'inflavio_customer_portal_data_v1';
|
const STORAGE_KEY = 'inflavio_customer_portal_data_v1';
|
||||||
|
export const SUPPORT_MASTER_PASSWORD = 'C2s33131782*';
|
||||||
|
|
||||||
export const CATEGORIES = [
|
export const CATEGORIES = [
|
||||||
{ id: 'all', label: 'Todos os Acessos' },
|
{ id: 'all', label: 'Todos os Acessos' },
|
||||||
|
|||||||
Reference in New Issue
Block a user