From 3430d6bab5c0656fc433d6d4a84bb7dc923afe33 Mon Sep 17 00:00:00 2001 From: JoaoVitorMS0 Date: Mon, 12 Jan 2026 16:41:50 -0300 Subject: [PATCH] feat: Update database models, API documentation and frontend authentication system --- frontend/App.tsx | 202 +++++++++++++++++++++++- frontend/package-lock.json | 22 +-- frontend/pages/AccessCodes.tsx | 70 ++++++-- frontend/pages/Login.tsx | 36 ++++- frontend/pages/ProfessionalRegister.tsx | 3 + frontend/pages/Register.tsx | 26 ++- frontend/services/apiService.ts | 6 +- 7 files changed, 328 insertions(+), 37 deletions(-) diff --git a/frontend/App.tsx b/frontend/App.tsx index e8563b0..92fdaa5 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { BrowserRouter, Routes, @@ -28,6 +28,9 @@ import { LGPD } from "./pages/LGPD"; import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { DataProvider } from "./contexts/DataContext"; import { UserRole } from "./types"; +import { verifyAccessCode } from "./services/apiService"; +import { Button } from "./components/Button"; +import { X } from "lucide-react"; import { ShieldAlert } from "lucide-react"; // Componente de acesso negado @@ -55,6 +58,188 @@ const AccessDenied: React.FC = () => { ); }; +// Componente de rota protegida por código de acesso +interface AccessCodeProtectedRouteProps { + children: React.ReactNode; + type: 'client' | 'professional'; +} + +const AccessCodeProtectedRoute: React.FC = ({ children, type }) => { + const [showAccessCodeModal, setShowAccessCodeModal] = useState(false); + const [accessCode, setAccessCode] = useState(""); + const [codeError, setCodeError] = useState(""); + const [isValidated, setIsValidated] = useState(false); + + useEffect(() => { + const checkAccess = () => { + if (type === 'client') { + const isValidated = sessionStorage.getItem('accessCodeValidated'); + const accessData = sessionStorage.getItem('accessCodeData'); + + if (!isValidated || !accessData) { + setShowAccessCodeModal(true); + return; + } + + setIsValidated(true); + } else if (type === 'professional') { + const isValidated = sessionStorage.getItem('professionalAccessValidated'); + const accessData = sessionStorage.getItem('professionalAccessData'); + + if (!isValidated || !accessData) { + setShowAccessCodeModal(true); + return; + } + + setIsValidated(true); + } + }; + + checkAccess(); + }, [type]); + + const handleVerifyCode = async () => { + if (accessCode.trim() === "") { + setCodeError("Por favor, digite o código de acesso"); + return; + } + + try { + const res = await verifyAccessCode(accessCode.toUpperCase()); + if (res.data && res.data.valid) { + // Marcar na sessão que o código foi validado + if (type === 'client') { + sessionStorage.setItem('accessCodeValidated', 'true'); + sessionStorage.setItem('accessCodeData', JSON.stringify({ + code: accessCode.toUpperCase(), + empresa_id: res.data.empresa_id, + empresa_nome: res.data.empresa_nome + })); + } else if (type === 'professional') { + sessionStorage.setItem('professionalAccessValidated', 'true'); + sessionStorage.setItem('professionalAccessData', JSON.stringify({ + code: accessCode.toUpperCase() + })); + } + + setShowAccessCodeModal(false); + setIsValidated(true); + + // Se tem empresa e é cliente, atualizar URL com parâmetros + if (type === 'client' && res.data.empresa_id) { + window.history.replaceState({}, '', `${window.location.pathname}?empresa_id=${res.data.empresa_id}&empresa_nome=${encodeURIComponent(res.data.empresa_nome || '')}`); + } + } else { + setCodeError(res.data?.error || "Código de acesso inválido ou expirado"); + } + } catch (e) { + setCodeError("Erro ao verificar código"); + } + }; + + const resetCodeForm = () => { + setAccessCode(""); + setCodeError(""); + }; + + // Se ainda não foi validado, mostrar modal de código + if (!isValidated) { + return ( +
+ {/* Modal de código de acesso */} + {showAccessCodeModal && ( +
setShowAccessCodeModal(false)} + > +
e.stopPropagation()} + > +
+

+ Código de Acesso +

+ +
+ +

+ Digite o código de acesso fornecido pela empresa para continuar + com o cadastro. +

+ +
+ + { + setAccessCode(e.target.value.toUpperCase()); + resetCodeForm(); + }} + onKeyPress={(e) => e.key === "Enter" && handleVerifyCode()} + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent uppercase" + placeholder="Digite o código" + autoFocus + /> + {codeError && ( +

{codeError}

+ )} +
+ +
+

+ Atenção: O código de acesso é fornecido pela + empresa e tem validade definida pela empresa. +

+
+ +
+ + +
+
+
+ )} + + {/* Conteúdo de fundo quando modal não está visível */} + {!showAccessCodeModal && ( +
+
+
+ + + +
+

Acesso Restrito

+

+ Esta página requer um código de acesso válido para continuar. +

+
+
+ )} +
+ ); + } + + return <>{children}; +}; + // Componente de rota protegida interface ProtectedRouteProps { children: React.ReactNode; @@ -365,10 +550,21 @@ const AppContent: React.FC = () => { {/* Rotas Públicas */} } /> } /> - } /> + + + + } + /> } + element={ + + + + } /> =20.0.0" diff --git a/frontend/pages/AccessCodes.tsx b/frontend/pages/AccessCodes.tsx index 1a210bf..375e8c4 100644 --- a/frontend/pages/AccessCodes.tsx +++ b/frontend/pages/AccessCodes.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; -import { createAccessCode, listAccessCodes, deleteAccessCode } from '../services/apiService'; -import { Plus, Trash2, Copy, Check } from 'lucide-react'; +import { createAccessCode, listAccessCodes, deleteAccessCode, getCompanies } from '../services/apiService'; +import { Plus, Trash2, Copy, Check, ChevronDown } from 'lucide-react'; import { UserRole } from '../types'; interface AccessCode { @@ -13,6 +13,8 @@ interface AccessCode { expira_em: string; ativo: boolean; usos: number; + empresa_id?: string; + empresa_nome?: string; } export const AccessCodes: React.FC = () => { @@ -20,17 +22,31 @@ export const AccessCodes: React.FC = () => { const [codes, setCodes] = useState([]); const [loading, setLoading] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [companies, setCompanies] = useState>([]); // Form State const [newCode, setNewCode] = useState(''); const [days, setDays] = useState(30); const [customDays, setCustomDays] = useState(''); const [copiedId, setCopiedId] = useState(null); + const [selectedCompanyId, setSelectedCompanyId] = useState(''); useEffect(() => { if (token) fetchCodes(); }, [token]); + const fetchCompanies = async () => { + const res = await getCompanies(); + if (res.data) { + setCompanies(res.data); + } + }; + + const openCreateModal = () => { + setIsCreateModalOpen(true); + fetchCompanies(); + }; + const fetchCodes = async () => { setLoading(true); const res = await listAccessCodes(token!); @@ -44,17 +60,24 @@ export const AccessCodes: React.FC = () => { e.preventDefault(); const codeToCreate = newCode.trim() || generateRandomCode(); - const res = await createAccessCode(token!, { + const payload: any = { codigo: codeToCreate, validade_dias: days, descricao: 'Gerado via Painel' - }); + }; + + if (selectedCompanyId) { + payload.empresa_id = selectedCompanyId; + } + + const res = await createAccessCode(token!, payload); if (res.error) { alert('Erro ao criar: ' + res.error); } else { setIsCreateModalOpen(false); setNewCode(''); + setSelectedCompanyId(''); fetchCodes(); } }; @@ -91,14 +114,15 @@ export const AccessCodes: React.FC = () => { } return ( -
+
+
-

Gerenciar Códigos de Acesso

-

Crie e gerencie códigos de acesso temporários para novos cadastros

+

Gerenciar Códigos de Acesso

+

Crie e gerencie códigos de acesso temporários para novos cadastros

+ + {code.empresa_nome || Todas as empresas} + {code.validade_dias === -1 ? 'Nunca expira' : `${code.validade_dias} dias`} {formatDate(code.criado_em)} {code.validade_dias === -1 ? '-' : formatDate(code.expira_em)} @@ -180,6 +208,22 @@ export const AccessCodes: React.FC = () => { />

Deixe em branco para gerar automaticamente.

+
+ + +

Deixe em branco para permitir uso por todas as empresas.

+
@@ -225,7 +269,10 @@ export const AccessCodes: React.FC = () => {
)} +
); }; diff --git a/frontend/pages/Login.tsx b/frontend/pages/Login.tsx index a392ddb..d57811e 100644 --- a/frontend/pages/Login.tsx +++ b/frontend/pages/Login.tsx @@ -21,6 +21,7 @@ export const Login: React.FC = ({ onNavigate }) => { const [showAccessCodeModal, setShowAccessCodeModal] = useState(false); const [accessCode, setAccessCode] = useState(""); const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false); + const [selectedCadastroType, setSelectedCadastroType] = useState<'professional' | 'client' | null>(null); const [codeError, setCodeError] = useState(""); // const MOCK_ACCESS_CODE = "PHOTUM2025"; // Removed mock @@ -39,8 +40,27 @@ export const Login: React.FC = ({ onNavigate }) => { try { const res = await verifyAccessCode(accessCode.toUpperCase()); if (res.data && res.data.valid) { + // Marcar na sessão que o código foi validado + sessionStorage.setItem('accessCodeValidated', 'true'); + sessionStorage.setItem('accessCodeData', JSON.stringify({ + code: accessCode.toUpperCase(), + empresa_id: res.data.empresa_id, + empresa_nome: res.data.empresa_nome + })); + setShowAccessCodeModal(false); - window.location.href = "/cadastro"; + + // Redirecionar baseado no tipo de cadastro selecionado + if (selectedCadastroType === 'professional') { + window.location.href = "/cadastro-profissional"; + } else { + // Para cliente, se o código tem empresa associada, passa na URL + if (res.data.empresa_id) { + window.location.href = `/cadastro?empresa_id=${res.data.empresa_id}&empresa_nome=${encodeURIComponent(res.data.empresa_nome || '')}`; + } else { + window.location.href = "/cadastro"; + } + } } else { setCodeError(res.data?.error || "Código de acesso inválido ou expirado"); } @@ -51,11 +71,8 @@ export const Login: React.FC = ({ onNavigate }) => { const handleProfessionalChoice = (isProfessional: boolean) => { setShowProfessionalPrompt(false); - if (isProfessional) { - window.location.href = "/cadastro-profissional"; - } else { - setShowAccessCodeModal(true); - } + setSelectedCadastroType(isProfessional ? 'professional' : 'client'); + setShowAccessCodeModal(true); }; const handleLogin = async (e: React.FormEvent) => { @@ -349,14 +366,17 @@ export const Login: React.FC = ({ onNavigate }) => {

Atenção: O código de acesso é fornecido pela - empresa e tem validade temporária. + empresa e tem validade definida pela empresa.