feat: add app lock screen requiring master password (C2s33131782*) on launch

This commit is contained in:
2026-10-15 13:39:37 -03:00
parent 8665992bdd
commit f01101cab0
4 changed files with 426 additions and 2 deletions
+23
View File
@@ -19,9 +19,17 @@ import OutputFiles from './components/OutputFiles';
import OpenSslReference from './components/OpenSslReference'; import OpenSslReference from './components/OpenSslReference';
import AppWPP from './components/wpp/AppWPP'; import AppWPP from './components/wpp/AppWPP';
import CustomerPortal from './components/portal/CustomerPortal'; import CustomerPortal from './components/portal/CustomerPortal';
import AppLockScreen from './components/AppLockScreen';
import { convertPfx } from './utils/certConverter'; import { convertPfx } from './utils/certConverter';
export default function App() { export default function App() {
const [isUnlocked, setIsUnlocked] = useState(() => {
return (
sessionStorage.getItem('inflavio_app_authenticated') === 'true' ||
localStorage.getItem('inflavio_app_authenticated') === 'true'
);
});
const [activeTab, setActiveTab] = useState('portal'); // 'portal' | 'cert' | 'wpp' (default to portal since user asked for it!) const [activeTab, setActiveTab] = useState('portal'); // 'portal' | 'cert' | 'wpp' (default to portal since user asked for it!)
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [arrayBuffer, setArrayBuffer] = useState(null); const [arrayBuffer, setArrayBuffer] = useState(null);
@@ -31,6 +39,16 @@ export default function App() {
const [conversionResult, setConversionResult] = useState(null); const [conversionResult, setConversionResult] = useState(null);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const handleUnlockApp = () => {
setIsUnlocked(true);
};
const handleLockApp = () => {
sessionStorage.removeItem('inflavio_app_authenticated');
localStorage.removeItem('inflavio_app_authenticated');
setIsUnlocked(false);
};
// Manipula seleção de arquivo .pfx // Manipula seleção de arquivo .pfx
const handleFileSelected = (selectedFile) => { const handleFileSelected = (selectedFile) => {
setFile(selectedFile); setFile(selectedFile);
@@ -93,12 +111,17 @@ export default function App() {
}, 80); }, 80);
}; };
if (!isUnlocked) {
return <AppLockScreen onUnlock={handleUnlockApp} />;
}
return ( return (
<div className="app-layout"> <div className="app-layout">
{/* Barra de Navegação Superior com Alternador de Apps */} {/* Barra de Navegação Superior com Alternador de Apps */}
<Header <Header
activeTab={activeTab} activeTab={activeTab}
onSelectTab={setActiveTab} onSelectTab={setActiveTab}
onLockApp={handleLockApp}
/> />
{/* Conteúdo Principal Dinâmico */} {/* Conteúdo Principal Dinâmico */}
+128
View File
@@ -0,0 +1,128 @@
import React, { useState, useRef, useEffect } from 'react';
import { Lock, Unlock, Eye, EyeOff, AlertCircle, ShieldCheck } from 'lucide-react';
import { SUPPORT_MASTER_PASSWORD } from '../utils/customerPortalData';
export default function AppLockScreen({ onUnlock }) {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState(false);
const [rememberDevice, setRememberDevice] = useState(true);
const [isShaking, setIsShaking] = useState(false);
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleUnlock = (e) => {
e?.preventDefault();
if (password === SUPPORT_MASTER_PASSWORD) {
setError(false);
if (rememberDevice) {
localStorage.setItem('inflavio_app_authenticated', 'true');
} else {
sessionStorage.setItem('inflavio_app_authenticated', 'true');
}
onUnlock();
} else {
setError(true);
setIsShaking(true);
setTimeout(() => setIsShaking(false), 500);
setPassword('');
inputRef.current?.focus();
}
};
return (
<div className="lockscreen-overlay">
<div className={`lockscreen-card ${isShaking ? 'lockscreen-shake' : ''}`}>
{/* Glow de Fundo */}
<div className="lockscreen-glow" />
{/* Topo: Logo & Identidade */}
<div className="lockscreen-header">
<div className="lockscreen-logo-wrapper">
<img
src="/c2logo.png"
alt="C2 Logo"
className="lockscreen-logo"
onError={(e) => {
e.target.style.display = 'none';
}}
/>
</div>
<div className="lockscreen-title-group">
<div className="lockscreen-title-row">
<h1 className="lockscreen-brand">InflavioApps</h1>
<span className="lockscreen-badge">Acesso Restrito</span>
</div>
<p className="lockscreen-sub">
Informe a senha padrão da equipe para acessar as aplicações.
</p>
</div>
</div>
{/* Formulário de Desbloqueio */}
<form onSubmit={handleUnlock} className="lockscreen-form">
<div className="lockscreen-input-group">
<div className="lockscreen-input-wrapper">
<Lock size={18} className="lockscreen-input-icon text-muted" />
<input
ref={inputRef}
type={showPassword ? 'text' : 'password'}
className={`password-input lockscreen-input ${error ? 'input-error' : ''}`}
placeholder="Digite a senha de acesso..."
value={password}
onChange={(e) => {
setPassword(e.target.value);
if (error) setError(false);
}}
autoComplete="current-password"
required
/>
<button
type="button"
className="btn-toggle-cred-pw lockscreen-eye-btn"
onClick={() => setShowPassword(!showPassword)}
title={showPassword ? 'Ocultar senha' : 'Ver senha'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{error && (
<div className="alert-error-box lockscreen-error">
<AlertCircle size={16} />
<span>Senha incorreta! Acesso restrito à equipe autorizada.</span>
</div>
)}
<div className="lockscreen-options">
<label className="lockscreen-remember-label">
<input
type="checkbox"
checked={rememberDevice}
onChange={(e) => setRememberDevice(e.target.checked)}
className="lockscreen-checkbox"
/>
<span>Lembrar autenticação neste navegador</span>
</label>
</div>
<button type="submit" className="btn-convert lockscreen-submit-btn">
<Unlock size={18} />
<span>Desbloquear Aplicação</span>
</button>
</form>
{/* Rodapé de Segurança */}
<div className="lockscreen-footer">
<ShieldCheck size={14} className="text-emerald" />
<span>Protegido com autenticação padrão C2 Sistemas</span>
</div>
</div>
</div>
);
}
+17 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ShieldCheck, MessageSquare, Bookmark } from 'lucide-react'; import { ShieldCheck, MessageSquare, Bookmark, Lock } from 'lucide-react';
export default function Header({ activeTab, onSelectTab }) { export default function Header({ activeTab, onSelectTab, onLockApp }) {
const getBrandDetails = () => { const getBrandDetails = () => {
switch (activeTab) { switch (activeTab) {
case 'cert': case 'cert':
@@ -80,6 +80,21 @@ export default function Header({ activeTab, onSelectTab }) {
<span className="nav-badge-pill pill-portal">23</span> <span className="nav-badge-pill pill-portal">23</span>
</button> </button>
</div> </div>
{/* Ações Rápidas: Bloquear / Sair */}
<div className="header-actions">
{onLockApp && (
<button
type="button"
className="btn-header-lock"
onClick={onLockApp}
title="Bloquear aplicação e exigir senha para reabrir"
>
<Lock size={14} />
<span>Bloquear</span>
</button>
)}
</div>
</div> </div>
</header> </header>
); );
+258
View File
@@ -2616,5 +2616,263 @@ body {
max-width: 440px; max-width: 440px;
} }
/* Header Lock / Sair Button */
.btn-header-lock {
display: inline-flex;
align-items: center;
gap: 0.4rem;
background-color: rgba(239, 68, 68, 0.08);
border: 1px solid rgba(239, 68, 68, 0.25);
color: #f87171;
padding: 0.45rem 0.75rem;
border-radius: 8px;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-header-lock:hover {
background-color: rgba(239, 68, 68, 0.18);
border-color: #ef4444;
color: #fca5a5;
box-shadow: 0 2px 10px rgba(239, 68, 68, 0.2);
}
/* App Lock Screen Styles */
.lockscreen-overlay {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
z-index: 99999;
background: radial-gradient(circle at 50% 30%, #0f172a 0%, #020617 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.lockscreen-card {
position: relative;
width: 100%;
max-width: 440px;
background: rgba(15, 23, 42, 0.85);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.7), 0 0 40px rgba(14, 165, 233, 0.1);
border-radius: 20px;
padding: 2.25rem 2rem;
backdrop-filter: blur(20px);
display: flex;
flex-direction: column;
gap: 1.5rem;
overflow: hidden;
}
.lockscreen-glow {
position: absolute;
top: -60px;
left: 50%;
transform: translateX(-50%);
width: 180px;
height: 120px;
background: radial-gradient(circle, rgba(14, 165, 233, 0.25) 0%, transparent 70%);
filter: blur(30px);
pointer-events: none;
}
.lockscreen-header {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 1rem;
}
.lockscreen-logo-wrapper {
width: 72px;
height: 72px;
background: rgba(30, 41, 59, 0.7);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
padding: 0.65rem;
}
.lockscreen-logo {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.lockscreen-title-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.lockscreen-title-row {
display: flex;
align-items: center;
gap: 0.6rem;
}
.lockscreen-brand {
font-size: 1.5rem;
font-weight: 700;
color: #f8fafc;
letter-spacing: -0.02em;
margin: 0;
}
.lockscreen-badge {
font-size: 0.675rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.2rem 0.5rem;
border-radius: 6px;
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.lockscreen-sub {
font-size: 0.85rem;
color: #94a3b8;
margin: 0;
max-width: 320px;
line-height: 1.4;
}
.lockscreen-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.lockscreen-input-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.lockscreen-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.lockscreen-input-icon {
position: absolute;
left: 0.875rem;
pointer-events: none;
}
.lockscreen-input {
width: 100%;
padding-left: 2.6rem !important;
padding-right: 2.75rem !important;
font-size: 0.95rem;
border-radius: 10px;
background: rgba(30, 41, 59, 0.85);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #f8fafc;
height: 46px;
transition: all 0.2s ease;
}
.lockscreen-input:focus {
border-color: #0ea5e9;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.2);
outline: none;
}
.lockscreen-eye-btn {
position: absolute;
right: 0.75rem;
padding: 0.35rem;
color: #94a3b8;
border-radius: 6px;
}
.lockscreen-eye-btn:hover {
color: #f8fafc;
}
.lockscreen-error {
margin: 0;
padding: 0.65rem 0.85rem;
font-size: 0.825rem;
border-radius: 8px;
}
.lockscreen-options {
display: flex;
align-items: center;
justify-content: flex-start;
}
.lockscreen-remember-label {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
color: #94a3b8;
cursor: pointer;
user-select: none;
}
.lockscreen-checkbox {
accent-color: #0ea5e9;
cursor: pointer;
}
.lockscreen-submit-btn {
width: 100%;
height: 46px;
justify-content: center;
font-size: 0.95rem;
font-weight: 600;
border-radius: 10px;
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
border: 1px solid rgba(56, 189, 248, 0.3);
box-shadow: 0 4px 15px rgba(2, 132, 199, 0.3);
cursor: pointer;
gap: 0.5rem;
}
.lockscreen-submit-btn:hover {
background: linear-gradient(135deg, #0369a1 0%, #075985 100%);
box-shadow: 0 6px 20px rgba(2, 132, 199, 0.4);
}
.lockscreen-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
font-size: 0.75rem;
color: #64748b;
border-top: 1px solid rgba(255, 255, 255, 0.05);
padding-top: 1rem;
}
/* Shake Animation on Wrong Password */
@keyframes lockscreenShake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-8px); }
40%, 80% { transform: translateX(8px); }
}
.lockscreen-shake {
animation: lockscreenShake 0.45s ease-in-out;
}