integrando os endpoints de login e logout #24
@ -9,8 +9,10 @@ export default function MainRoutesLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
console.log('[MAIN-ROUTES-LAYOUT] Layout do administrador carregado')
|
||||
|
||||
return (
|
||||
<ProtectedRoute requiredUserType="administrador">
|
||||
<ProtectedRoute requiredUserType={["administrador"]}>
|
||||
<div className="min-h-screen bg-background flex">
|
||||
<SidebarProvider>
|
||||
<Sidebar />
|
||||
|
||||
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AuthenticationError } from '@/lib/auth'
|
||||
|
||||
export default function LoginAdminPage() {
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||
@ -20,29 +21,27 @@ export default function LoginAdminPage() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
// Simular delay de autenticação
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Tentar fazer login usando o contexto com tipo administrador
|
||||
const success = login(credentials.email, credentials.password, 'administrador')
|
||||
|
||||
if (success) {
|
||||
// Redirecionar para o dashboard do administrador
|
||||
setTimeout(() => {
|
||||
router.push('/dashboard')
|
||||
try {
|
||||
// Tentar fazer login usando o contexto com tipo administrador
|
||||
const success = await login(credentials.email, credentials.password, 'administrador')
|
||||
|
||||
if (success) {
|
||||
console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...')
|
||||
|
||||
// Fallback: usar window.location se router.push não funcionar
|
||||
setTimeout(() => {
|
||||
if (window.location.pathname === '/login-admin') {
|
||||
window.location.href = '/dashboard'
|
||||
}
|
||||
}, 100)
|
||||
}, 100)
|
||||
} else {
|
||||
setError('Email ou senha incorretos')
|
||||
// Redirecionamento direto - solução que funcionou
|
||||
window.location.href = '/dashboard'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LOGIN-ADMIN] Erro no login:', err)
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Erro inesperado. Tente novamente.')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -75,6 +74,7 @@ export default function LoginAdminPage() {
|
||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -90,6 +90,7 @@ export default function LoginAdminPage() {
|
||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
122
susconecta/app/login-paciente/page.tsx
Normal file
122
susconecta/app/login-paciente/page.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AuthenticationError } from '@/lib/auth'
|
||||
|
||||
export default function LoginPacientePage() {
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
// Tentar fazer login usando o contexto com tipo paciente
|
||||
const success = await login(credentials.email, credentials.password, 'paciente')
|
||||
|
||||
if (success) {
|
||||
// Redirecionar para a página do paciente
|
||||
router.push('/paciente')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LOGIN-PACIENTE] Erro no login:', err)
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Erro inesperado. Tente novamente.')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">
|
||||
Portal do Paciente
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Acesse sua área pessoal e gerencie suas consultas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Digite seu email"
|
||||
value={credentials.email}
|
||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Senha
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Digite sua senha"
|
||||
value={credentials.password}
|
||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full cursor-pointer"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar na Minha Área'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button variant="outline" asChild className="w-full">
|
||||
<Link href="/">
|
||||
Voltar ao Início
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AuthenticationError } from '@/lib/auth'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||
@ -20,29 +21,27 @@ export default function LoginPage() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
// Simular delay de autenticação
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Tentar fazer login usando o contexto com tipo profissional
|
||||
const success = login(credentials.email, credentials.password, 'profissional')
|
||||
|
||||
if (success) {
|
||||
// Redirecionar para a página do profissional
|
||||
setTimeout(() => {
|
||||
router.push('/profissional')
|
||||
try {
|
||||
// Tentar fazer login usando o contexto com tipo profissional
|
||||
const success = await login(credentials.email, credentials.password, 'profissional')
|
||||
|
||||
if (success) {
|
||||
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
|
||||
|
||||
// Fallback: usar window.location se router.push não funcionar
|
||||
setTimeout(() => {
|
||||
if (window.location.pathname === '/login') {
|
||||
window.location.href = '/profissional'
|
||||
}
|
||||
}, 100)
|
||||
}, 100)
|
||||
} else {
|
||||
setError('Email ou senha incorretos')
|
||||
// Redirecionamento direto - solução que funcionou
|
||||
window.location.href = '/profissional'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Erro inesperado. Tente novamente.')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -75,6 +74,7 @@ export default function LoginPage() {
|
||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -90,6 +90,7 @@ export default function LoginPage() {
|
||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
95
susconecta/app/paciente/page.tsx
Normal file
95
susconecta/app/paciente/page.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { User, LogOut, Home } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||
|
||||
export default function PacientePage() {
|
||||
const { logout, user } = useAuth()
|
||||
|
||||
const handleLogout = async () => {
|
||||
console.log('[PACIENTE] Iniciando logout...')
|
||||
await logout()
|
||||
}
|
||||
|
||||
return (
|
||||
<ProtectedRoute requiredUserType={["paciente"]}>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md shadow-lg">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
||||
<User className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold text-gray-900">
|
||||
Portal do Paciente
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-600">
|
||||
Bem-vindo ao seu espaço pessoal
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* Informações do Paciente */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-2">
|
||||
Maria Silva Santos
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
CPF: 123.456.789-00
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
Idade: 35 anos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Informações do Login */}
|
||||
<div className="bg-gray-100 rounded-lg p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600 mb-1">
|
||||
Conectado como:
|
||||
</p>
|
||||
<p className="font-medium text-gray-800">
|
||||
{user?.email || 'paciente@example.com'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Tipo de usuário: Paciente
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão Voltar ao Início */}
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
<Link href="/">
|
||||
<Home className="h-4 w-4" />
|
||||
Voltar ao Início
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* Botão de Logout */}
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
variant="destructive"
|
||||
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Sair
|
||||
</Button>
|
||||
|
||||
{/* Informação adicional */}
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-gray-500">
|
||||
Em breve, mais funcionalidades estarão disponíveis
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
83
susconecta/app/test-supabase/page.tsx
Normal file
83
susconecta/app/test-supabase/page.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react';
|
||||
import { testSupabaseConnection, simpleLogin } from '@/lib/simple-auth';
|
||||
|
||||
export default function TestPage() {
|
||||
const [email, setEmail] = useState('test@example.com');
|
||||
const [password, setPassword] = useState('123456');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setResult('Testando conexão...');
|
||||
const success = await testSupabaseConnection();
|
||||
setResult(success ? '✅ Conexão OK' : '❌ Conexão falhou');
|
||||
};
|
||||
|
||||
const handleSimpleLogin = async () => {
|
||||
setResult('Tentando login...');
|
||||
try {
|
||||
const response = await simpleLogin(email, password);
|
||||
setResult(`✅ Login OK: ${JSON.stringify(response, null, 2)}`);
|
||||
} catch (error) {
|
||||
setResult(`❌ Login falhou: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-8">
|
||||
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">🔧 Teste de Conexão Supabase</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handleTestConnection}
|
||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600"
|
||||
>
|
||||
1. Testar Conexão com Supabase
|
||||
</button>
|
||||
|
||||
<div className="border p-4 rounded">
|
||||
<h3 className="font-semibold mb-2">2. Testar Login:</h3>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full border rounded px-3 py-2"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Senha"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full border rounded px-3 py-2"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSimpleLogin}
|
||||
className="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600"
|
||||
>
|
||||
Testar Login Simples
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 p-4 rounded">
|
||||
<h3 className="font-semibold mb-2">Resultado:</h3>
|
||||
<pre className="text-sm overflow-auto max-h-96">{result}</pre>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600">
|
||||
<p><strong>Como usar:</strong></p>
|
||||
<ol className="list-decimal list-inside space-y-1">
|
||||
<li>Primeiro teste a conexão básica</li>
|
||||
<li>Se OK, teste o login (qualquer email/senha por enquanto)</li>
|
||||
<li>Veja os logs no console (F12) para mais detalhes</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,43 +1,137 @@
|
||||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import type { UserType } from '@/types/auth'
|
||||
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
requiredUserType?: string
|
||||
requiredUserType?: UserType[]
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({ children, requiredUserType }: ProtectedRouteProps) {
|
||||
const { isAuthenticated, userType, checkAuth } = useAuth()
|
||||
export default function ProtectedRoute({
|
||||
children,
|
||||
requiredUserType
|
||||
}: ProtectedRouteProps) {
|
||||
const { authStatus, user } = useAuth()
|
||||
const router = useRouter()
|
||||
const isRedirecting = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
}, [checkAuth])
|
||||
// Evitar múltiplos redirects
|
||||
if (isRedirecting.current) return
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
console.log('Usuário não autenticado, redirecionando para login...')
|
||||
router.push('/login')
|
||||
} else if (requiredUserType && userType !== requiredUserType) {
|
||||
console.log(`Tipo de usuário incorreto. Esperado: ${requiredUserType}, Atual: ${userType}`)
|
||||
router.push('/login')
|
||||
} else {
|
||||
console.log('Usuário autenticado!')
|
||||
// Durante loading, não fazer nada
|
||||
if (authStatus === 'loading') return
|
||||
|
||||
// Se não autenticado, redirecionar para login
|
||||
if (authStatus === 'unauthenticated') {
|
||||
isRedirecting.current = true
|
||||
|
||||
console.log('[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...')
|
||||
|
||||
// Determinar página de login baseada no histórico
|
||||
let userType: UserType = 'profissional'
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const storedUserType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
|
||||
if (storedUserType && ['profissional', 'paciente', 'administrador'].includes(storedUserType)) {
|
||||
userType = storedUserType as UserType
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[PROTECTED-ROUTE] Erro ao ler localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loginRoute = LOGIN_ROUTES[userType]
|
||||
console.log('[PROTECTED-ROUTE] Redirecionando para login:', {
|
||||
userType,
|
||||
loginRoute,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
|
||||
router.push(loginRoute)
|
||||
return
|
||||
}
|
||||
}, [isAuthenticated, userType, requiredUserType, router])
|
||||
|
||||
if (!isAuthenticated || (requiredUserType && userType !== requiredUserType)) {
|
||||
// Se autenticado mas não tem permissão para esta página
|
||||
if (authStatus === 'authenticated' && user && requiredUserType && !requiredUserType.includes(user.userType)) {
|
||||
isRedirecting.current = true
|
||||
|
||||
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página', {
|
||||
userType: user.userType,
|
||||
requiredTypes: requiredUserType
|
||||
})
|
||||
|
||||
const correctRoute = USER_TYPE_ROUTES[user.userType]
|
||||
console.log('[PROTECTED-ROUTE] Redirecionando para área correta:', correctRoute)
|
||||
|
||||
router.push(correctRoute)
|
||||
return
|
||||
}
|
||||
|
||||
// Se chegou aqui, acesso está autorizado
|
||||
if (authStatus === 'authenticated') {
|
||||
console.log('[PROTECTED-ROUTE] ACESSO AUTORIZADO!', {
|
||||
userType: user?.userType,
|
||||
email: user?.email,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
isRedirecting.current = false
|
||||
}
|
||||
}, [authStatus, user, requiredUserType, router])
|
||||
|
||||
// Durante loading, mostrar spinner
|
||||
if (authStatus === 'loading') {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Redirecionando para login...</p>
|
||||
<p className="mt-4 text-gray-600">Verificando autenticação...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Se não autenticado ou redirecionando, mostrar spinner
|
||||
if (authStatus === 'unauthenticated' || isRedirecting.current) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Redirecionando...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Se usuário não tem permissão, mostrar fallback (não deveria chegar aqui devido ao useEffect)
|
||||
if (requiredUserType && user && !requiredUserType.includes(user.userType)) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Acesso Negado</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Você não tem permissão para acessar esta página.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mb-6">
|
||||
Tipo de acesso necessário: {requiredUserType.join(' ou ')}
|
||||
<br />
|
||||
Seu tipo de acesso: {user.userType}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push(USER_TYPE_ROUTES[user.userType])}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 cursor-pointer"
|
||||
>
|
||||
Ir para minha área
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Finalmente, renderizar conteúdo protegido
|
||||
return <>{children}</>
|
||||
}
|
||||
@ -9,7 +9,7 @@ import { useState, useEffect, useRef } from "react"
|
||||
import { SidebarTrigger } from "../ui/sidebar"
|
||||
|
||||
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
||||
const { logout, userEmail, userType } = useAuth();
|
||||
const { logout, user } = useAuth();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -69,15 +69,15 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-semibold leading-none">
|
||||
{userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'}
|
||||
{user?.userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'}
|
||||
</p>
|
||||
{userEmail ? (
|
||||
<p className="text-xs leading-none text-gray-600">{userEmail}</p>
|
||||
{user?.email ? (
|
||||
<p className="text-xs leading-none text-gray-600">{user.email}</p>
|
||||
) : (
|
||||
<p className="text-xs leading-none text-gray-600">Email não disponível</p>
|
||||
)}
|
||||
<p className="text-xs leading-none text-blue-600 font-medium">
|
||||
Tipo: {userType === 'administrador' ? 'Administrador' : userType || 'Não definido'}
|
||||
Tipo: {user?.userType === 'administrador' ? 'Administrador' : user?.userType || 'Não definido'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,15 +95,8 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
||||
e.preventDefault();
|
||||
setDropdownOpen(false);
|
||||
|
||||
// Logout específico para administrador
|
||||
if (userType === 'administrador') {
|
||||
localStorage.removeItem('isAuthenticated');
|
||||
localStorage.removeItem('userEmail');
|
||||
localStorage.removeItem('userType');
|
||||
window.location.href = '/login-admin';
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
|
||||
logout();
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 cursor-pointer"
|
||||
>
|
||||
|
||||
@ -46,8 +46,9 @@ export function Header() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
|
||||
asChild
|
||||
>
|
||||
Sou Paciente
|
||||
<Link href="/login-paciente">Sou Paciente</Link>
|
||||
</Button>
|
||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
||||
@ -94,8 +95,9 @@ export function Header() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
|
||||
asChild
|
||||
>
|
||||
Sou Paciente
|
||||
<Link href="/login-paciente">Sou Paciente</Link>
|
||||
</Button>
|
||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full">
|
||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
||||
|
||||
@ -4,20 +4,20 @@ import Link from "next/link"
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="py-16 lg:py-24 bg-background">
|
||||
<section className="py-8 lg:py-12 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="grid lg:grid-cols-2 gap-8 items-center">
|
||||
{}
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
||||
APROXIMANDO MÉDICOS E PACIENTES
|
||||
</div>
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-foreground leading-tight text-balance">
|
||||
<h1 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance">
|
||||
Segurança, <span className="text-primary">Confiabilidade</span> e{" "}
|
||||
<span className="text-primary">Rapidez</span>
|
||||
</h1>
|
||||
<div className="space-y-2 text-lg text-muted-foreground">
|
||||
<div className="space-y-1 text-base text-muted-foreground">
|
||||
<p>Experimente o futuro dos agendamentos.</p>
|
||||
<p>Encontre profissionais capacitados e marque já sua consulta.</p>
|
||||
</div>
|
||||
@ -25,33 +25,38 @@ export function HeroSection() {
|
||||
|
||||
{}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button size="lg" className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||
Sou Paciente
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<Link href="/login-paciente">Portal do Paciente</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
|
||||
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<Link href="/profissional">Sou Profissional de Saúde</Link>
|
||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="relative">
|
||||
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-8">
|
||||
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-6">
|
||||
<img
|
||||
src="/medico-sorridente-de-tiro-medio-vestindo-casaco.jpg"
|
||||
alt="Médico profissional sorrindo"
|
||||
className="w-full h-auto rounded-lg"
|
||||
className="w-full h-auto rounded-lg min-h-80 max-h-[500px] object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="mt-16 grid md:grid-cols-3 gap-8">
|
||||
<div className="mt-10 grid md:grid-cols-3 gap-6">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<Shield className="w-4 h-4 text-primary" />
|
||||
|
||||
@ -1,100 +1,243 @@
|
||||
'use client'
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
|
||||
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean
|
||||
userEmail: string | null
|
||||
userType: string | null
|
||||
login: (email: string, password: string, userType: string) => boolean
|
||||
logout: () => void
|
||||
checkAuth: () => void
|
||||
}
|
||||
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
|
||||
import { isExpired, parseJwt } from '@/lib/jwt'
|
||||
import { httpClient } from '@/lib/http'
|
||||
import type {
|
||||
AuthContextType,
|
||||
UserData,
|
||||
AuthStatus,
|
||||
UserType
|
||||
} from '@/types/auth'
|
||||
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth'
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null)
|
||||
const [userType, setUserType] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>('loading')
|
||||
const [user, setUser] = useState<UserData | null>(null)
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
const hasInitialized = useRef(false)
|
||||
|
||||
const checkAuth = () => {
|
||||
// Utilitários de armazenamento memorizados
|
||||
const clearAuthData = useCallback(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const auth = localStorage.getItem('isAuthenticated')
|
||||
const email = localStorage.getItem('userEmail')
|
||||
const type = localStorage.getItem('userType')
|
||||
|
||||
if (auth === 'true' && email) {
|
||||
setIsAuthenticated(true)
|
||||
setUserEmail(email)
|
||||
setUserType(type)
|
||||
} else {
|
||||
setIsAuthenticated(false)
|
||||
setUserEmail(null)
|
||||
setUserType(null)
|
||||
}
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.USER)
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||
// Manter USER_TYPE para redirecionamento correto
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
setAuthStatus('unauthenticated')
|
||||
console.log('[AUTH] Dados de autenticação limpos - logout realizado')
|
||||
}, [])
|
||||
|
||||
const login = (email: string, password: string, userType: string): boolean => {
|
||||
if (email === 'teste@gmail.com' && password === '123456') {
|
||||
localStorage.setItem('isAuthenticated', 'true')
|
||||
localStorage.setItem('userEmail', email)
|
||||
localStorage.setItem('userType', userType)
|
||||
setIsAuthenticated(true)
|
||||
setUserEmail(email)
|
||||
setUserType(userType)
|
||||
const saveAuthData = useCallback((
|
||||
accessToken: string,
|
||||
userData: UserData,
|
||||
refreshToken?: string
|
||||
) => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Persistir dados de forma atômica
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken)
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType)
|
||||
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
setToken(accessToken)
|
||||
setUser(userData)
|
||||
setAuthStatus('authenticated')
|
||||
|
||||
console.log('[AUTH] LOGIN realizado - Dados salvos!', {
|
||||
userType: userData.userType,
|
||||
email: userData.email,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao salvar dados:', error)
|
||||
clearAuthData()
|
||||
}
|
||||
}, [clearAuthData])
|
||||
|
||||
// Verificação inicial de autenticação
|
||||
const checkAuth = useCallback(async (): Promise<void> => {
|
||||
if (typeof window === 'undefined') {
|
||||
setAuthStatus('unauthenticated')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER)
|
||||
|
||||
console.log('[AUTH] Verificando sessão...', {
|
||||
hasToken: !!storedToken,
|
||||
hasUser: !!storedUser,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
|
||||
// Pequeno delay para visualizar logs
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
if (!storedToken || !storedUser) {
|
||||
console.log('[AUTH] Dados ausentes - sessão inválida')
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
clearAuthData()
|
||||
return
|
||||
}
|
||||
|
||||
// Verificar se token está expirado
|
||||
if (isExpired(storedToken)) {
|
||||
console.log('[AUTH] Token expirado - tentando renovar...')
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||
if (refreshToken && !isExpired(refreshToken)) {
|
||||
// Tentar renovar via HTTP client (que já tem a lógica)
|
||||
try {
|
||||
await httpClient.get('/auth/v1/me') // Trigger refresh se necessário
|
||||
|
||||
// Se chegou aqui, refresh foi bem-sucedido
|
||||
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
const userData = JSON.parse(storedUser) as UserData
|
||||
|
||||
if (newToken && newToken !== storedToken) {
|
||||
setToken(newToken)
|
||||
setUser(userData)
|
||||
setAuthStatus('authenticated')
|
||||
console.log('[AUTH] Token RENOVADO automaticamente!')
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
return
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.log('❌ [AUTH] Falha no refresh automático')
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
}
|
||||
|
||||
clearAuthData()
|
||||
return
|
||||
}
|
||||
|
||||
// Restaurar sessão válida
|
||||
const userData = JSON.parse(storedUser) as UserData
|
||||
setToken(storedToken)
|
||||
setUser(userData)
|
||||
setAuthStatus('authenticated')
|
||||
|
||||
console.log('[AUTH] Sessão RESTAURADA com sucesso!', {
|
||||
userId: userData.id,
|
||||
userType: userData.userType,
|
||||
email: userData.email,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro na verificação:', error)
|
||||
clearAuthData()
|
||||
}
|
||||
}, [clearAuthData])
|
||||
|
||||
// Login memoizado
|
||||
const login = useCallback(async (
|
||||
email: string,
|
||||
password: string,
|
||||
userType: UserType
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
console.log('[AUTH] Iniciando login:', { email, userType })
|
||||
|
||||
const response = await loginUser(email, password, userType)
|
||||
|
||||
saveAuthData(
|
||||
response.access_token,
|
||||
response.user,
|
||||
response.refresh_token
|
||||
)
|
||||
|
||||
console.log('[AUTH] Login realizado com sucesso')
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro no login:', error)
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Erro inesperado durante o login',
|
||||
'UNKNOWN_ERROR',
|
||||
error
|
||||
)
|
||||
}
|
||||
}, [saveAuthData])
|
||||
|
||||
// Logout memoizado
|
||||
const logout = useCallback(async (): Promise<void> => {
|
||||
console.log('[AUTH] Iniciando logout')
|
||||
|
||||
const currentUserType = user?.userType ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) : null) ||
|
||||
'profissional'
|
||||
|
||||
try {
|
||||
if (token) {
|
||||
await logoutUser(token)
|
||||
console.log('[AUTH] Logout realizado na API')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro no logout da API:', error)
|
||||
}
|
||||
|
||||
clearAuthData()
|
||||
|
||||
// Redirecionamento baseado no tipo de usuário
|
||||
const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || '/login'
|
||||
|
||||
console.log('[AUTH] Redirecionando para:', loginRoute)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = loginRoute
|
||||
}
|
||||
}, [user?.userType, token, clearAuthData])
|
||||
|
||||
// Refresh token memoizado (usado pelo HTTP client)
|
||||
const refreshToken = useCallback(async (): Promise<boolean> => {
|
||||
// Esta função é principalmente para compatibilidade
|
||||
// O refresh real é feito pelo HTTP client
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const logout = () => {
|
||||
// Usar o estado atual em vez do localStorage para evitar condição de corrida
|
||||
const currentUserType = userType || localStorage.getItem('userType')
|
||||
|
||||
localStorage.removeItem('isAuthenticated')
|
||||
localStorage.removeItem('userEmail')
|
||||
localStorage.removeItem('userType')
|
||||
setIsAuthenticated(false)
|
||||
setUserEmail(null)
|
||||
setUserType(null)
|
||||
|
||||
// Redirecionar para a página de login correta baseado no tipo de usuário
|
||||
if (currentUserType === 'administrador') {
|
||||
router.push('/login-admin')
|
||||
} else {
|
||||
router.push('/login')
|
||||
// Getters memorizados
|
||||
const contextValue = useMemo(() => ({
|
||||
authStatus,
|
||||
user,
|
||||
token,
|
||||
login,
|
||||
logout,
|
||||
refreshToken
|
||||
}), [authStatus, user, token, login, logout, refreshToken])
|
||||
|
||||
// Inicialização única
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && typeof window !== 'undefined') {
|
||||
hasInitialized.current = true
|
||||
checkAuth()
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Carregando...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}, [checkAuth])
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
isAuthenticated,
|
||||
userEmail,
|
||||
userType,
|
||||
login,
|
||||
logout,
|
||||
checkAuth
|
||||
}}>
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
|
||||
@ -82,10 +82,24 @@ export const PATHS = {
|
||||
} as const;
|
||||
|
||||
|
||||
// Função para obter o token JWT do localStorage
|
||||
function getAuthToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('auth_token');
|
||||
}
|
||||
|
||||
function headers(kind: "json" | "form" = "json"): Record<string, string> {
|
||||
const h: Record<string, string> = {};
|
||||
const token = process.env.NEXT_PUBLIC_API_TOKEN?.trim();
|
||||
if (token) h.Authorization = `Bearer ${token}`;
|
||||
|
||||
// API Key da Supabase sempre necessária
|
||||
h.apikey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
||||
|
||||
// Bearer Token quando usuário está logado
|
||||
const jwtToken = getAuthToken();
|
||||
if (jwtToken) {
|
||||
h.Authorization = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
if (kind === "json") h["Content-Type"] = "application/json";
|
||||
return h;
|
||||
}
|
||||
|
||||
388
susconecta/lib/auth.ts
Normal file
388
susconecta/lib/auth.ts
Normal file
@ -0,0 +1,388 @@
|
||||
import type {
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
RefreshTokenResponse,
|
||||
AuthError,
|
||||
UserData
|
||||
} from '@/types/auth';
|
||||
|
||||
import { API_CONFIG, AUTH_ENDPOINTS, DEFAULT_HEADERS, API_KEY, buildApiUrl } from '@/lib/config';
|
||||
import { debugRequest } from '@/lib/debug-utils';
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
|
||||
/**
|
||||
* Classe de erro customizada para autenticação
|
||||
*/
|
||||
export class AuthenticationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public details?: any
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AuthenticationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers para requisições autenticadas (COM Bearer token)
|
||||
*/
|
||||
function getAuthHeaders(token: string): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
"Authorization": `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers APENAS para login (SEM Authorization Bearer)
|
||||
*/
|
||||
function getLoginHeaders(): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilitário para processar resposta da API
|
||||
*/
|
||||
async function processResponse<T>(response: Response): Promise<T> {
|
||||
console.log(`[AUTH] Response status: ${response.status} ${response.statusText}`);
|
||||
|
||||
let data: any = null;
|
||||
|
||||
try {
|
||||
const text = await response.text();
|
||||
if (text) {
|
||||
data = JSON.parse(text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[AUTH] Response sem JSON ou vazia (normal para alguns endpoints)');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data?.message || data?.error || response.statusText || 'Erro na autenticação';
|
||||
const errorCode = data?.code || String(response.status);
|
||||
|
||||
console.error('[AUTH ERROR]', {
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
data,
|
||||
});
|
||||
|
||||
throw new AuthenticationError(errorMessage, errorCode, data);
|
||||
}
|
||||
|
||||
console.log('[AUTH] Response data:', data);
|
||||
return data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serviço para fazer login e obter token JWT
|
||||
*/
|
||||
export async function loginUser(
|
||||
email: string,
|
||||
password: string,
|
||||
userType: 'profissional' | 'paciente' | 'administrador'
|
||||
): Promise<LoginResponse> {
|
||||
let url = AUTH_ENDPOINTS.LOGIN;
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
|
||||
console.log('[AUTH-API] Iniciando login...', {
|
||||
email,
|
||||
userType,
|
||||
url,
|
||||
payload,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
// Delay para visualizar na aba Network
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
try {
|
||||
console.log('[AUTH-API] Enviando requisição de login...');
|
||||
|
||||
// Debug: Log request sem credenciais sensíveis
|
||||
debugRequest('POST', url, getLoginHeaders(), payload);
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getLoginHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
// Se login falhar com 400, tentar criar usuário automaticamente
|
||||
if (!response.ok && response.status === 400) {
|
||||
console.log('[AUTH-API] Login falhou (400), tentando criar usuário...');
|
||||
|
||||
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
|
||||
const signupPayload = {
|
||||
email,
|
||||
password,
|
||||
data: {
|
||||
userType: userType,
|
||||
name: email.split('@')[0],
|
||||
}
|
||||
};
|
||||
|
||||
debugRequest('POST', signupUrl, getLoginHeaders(), signupPayload);
|
||||
|
||||
const signupResponse = await fetch(signupUrl, {
|
||||
method: 'POST',
|
||||
headers: getLoginHeaders(),
|
||||
body: JSON.stringify(signupPayload),
|
||||
});
|
||||
|
||||
if (signupResponse.ok) {
|
||||
console.log('[AUTH-API] Usuário criado, tentando login novamente...');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getLoginHeaders(),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, {
|
||||
url: response.url,
|
||||
status: response.status,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
// Se ainda for 400, mostrar detalhes do erro
|
||||
if (!response.ok) {
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
console.error('[AUTH-API] Erro detalhado:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorText,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[AUTH-API] Não foi possível ler erro da resposta');
|
||||
}
|
||||
}
|
||||
|
||||
// Delay adicional para ver status code
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
const data = await processResponse<any>(response);
|
||||
|
||||
console.log('[AUTH] Dados recebidos da API:', data);
|
||||
|
||||
// Verificar se recebemos os dados necessários
|
||||
if (!data || (!data.access_token && !data.token)) {
|
||||
console.error('[AUTH] API não retornou token válido:', data);
|
||||
throw new AuthenticationError(
|
||||
'API não retornou token de acesso',
|
||||
'NO_TOKEN_RECEIVED',
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
// Adaptar resposta da sua API para o formato esperado
|
||||
const adaptedResponse: LoginResponse = {
|
||||
access_token: data.access_token || data.token,
|
||||
token_type: data.token_type || "Bearer",
|
||||
expires_in: data.expires_in || 3600,
|
||||
user: {
|
||||
id: data.user?.id || data.id || "1",
|
||||
email: email,
|
||||
name: data.user?.name || data.name || email.split('@')[0],
|
||||
userType: userType,
|
||||
profile: data.user?.profile || data.profile || {}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[AUTH-API] LOGIN REALIZADO COM SUCESSO!', {
|
||||
token: adaptedResponse.access_token?.substring(0, 20) + '...',
|
||||
user: {
|
||||
email: adaptedResponse.user.email,
|
||||
userType: adaptedResponse.user.userType
|
||||
},
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
// Delay final para visualizar sucesso
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
return adaptedResponse;
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro no login:', error);
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Email ou senha incorretos',
|
||||
'INVALID_CREDENTIALS',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serviço para fazer logout do usuário
|
||||
*/
|
||||
export async function logoutUser(token: string): Promise<void> {
|
||||
const url = AUTH_ENDPOINTS.LOGOUT;
|
||||
|
||||
console.log('[AUTH-API] Fazendo logout na API...', {
|
||||
url,
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
// Delay para visualizar na aba Network
|
||||
await new Promise(resolve => setTimeout(resolve, 400));
|
||||
|
||||
try {
|
||||
console.log('[AUTH-API] Enviando requisição de logout...');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(token),
|
||||
});
|
||||
|
||||
console.log(`[AUTH-API] Logout response: ${response.status} ${response.statusText}`, {
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
// Delay para ver status code
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
|
||||
// Logout pode retornar 200, 204 ou até 401 (se token já expirou)
|
||||
// Todos são considerados "sucesso" para logout
|
||||
if (response.ok || response.status === 401) {
|
||||
console.log('[AUTH] Logout realizado com sucesso na API');
|
||||
return;
|
||||
}
|
||||
|
||||
// Se chegou aqui, algo deu errado mas não é crítico para logout
|
||||
console.warn('[AUTH] API retornou status inesperado:', response.status);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao chamar API de logout:', error);
|
||||
}
|
||||
|
||||
// Para logout, sempre continuamos mesmo com erro na API
|
||||
// Isso evita que o usuário fique "preso" se a API estiver indisponível
|
||||
console.log('[AUTH] Logout concluído (local sempre executado)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serviço para renovar token JWT
|
||||
*/
|
||||
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> {
|
||||
const url = AUTH_ENDPOINTS.REFRESH;
|
||||
|
||||
console.log('[AUTH] Renovando token');
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"apikey": API_KEY,
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
const data = await processResponse<RefreshTokenResponse>(response);
|
||||
|
||||
console.log('[AUTH] Token renovado com sucesso');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao renovar token:', error);
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Não foi possível renovar a sessão',
|
||||
'REFRESH_ERROR',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serviço para obter dados do usuário atual
|
||||
*/
|
||||
export async function getCurrentUser(token: string): Promise<UserData> {
|
||||
const url = AUTH_ENDPOINTS.USER;
|
||||
|
||||
console.log('[AUTH] Obtendo dados do usuário atual');
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders(token),
|
||||
});
|
||||
|
||||
const data = await processResponse<UserData>(response);
|
||||
|
||||
console.log('[AUTH] Dados do usuário obtidos:', { id: data.id, email: data.email });
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[AUTH] Erro ao obter usuário atual:', error);
|
||||
|
||||
if (error instanceof AuthenticationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AuthenticationError(
|
||||
'Não foi possível obter dados do usuário',
|
||||
'USER_DATA_ERROR',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilitário para validar se um token está expirado
|
||||
*/
|
||||
export function isTokenExpired(expiryTimestamp: number): boolean {
|
||||
const now = Date.now();
|
||||
const expiry = expiryTimestamp * 1000; // Converter para milliseconds
|
||||
const buffer = 5 * 60 * 1000; // Buffer de 5 minutos
|
||||
|
||||
return now >= (expiry - buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilitário para interceptar requests e adicionar token automaticamente
|
||||
*/
|
||||
export function createAuthenticatedFetch(getToken: () => string | null) {
|
||||
return async (url: string, options: RequestInit = {}): Promise<Response> => {
|
||||
const token = getToken();
|
||||
|
||||
if (token) {
|
||||
const headers = {
|
||||
...options.headers,
|
||||
...getAuthHeaders(token),
|
||||
};
|
||||
|
||||
options = {
|
||||
...options,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
return fetch(url, options);
|
||||
};
|
||||
}
|
||||
22
susconecta/lib/config.ts
Normal file
22
susconecta/lib/config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ENV_CONFIG } from './env-config';
|
||||
|
||||
export const API_CONFIG = {
|
||||
BASE_URL: ENV_CONFIG.SUPABASE_URL + "/rest/v1",
|
||||
TIMEOUT: 30000,
|
||||
VERSION: "v1",
|
||||
} as const;
|
||||
|
||||
export const AUTH_ENDPOINTS = ENV_CONFIG.AUTH_ENDPOINTS;
|
||||
|
||||
export const API_KEY = ENV_CONFIG.SUPABASE_ANON_KEY;
|
||||
|
||||
export const DEFAULT_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
} as const;
|
||||
|
||||
export function buildApiUrl(endpoint: string): string {
|
||||
const baseUrl = API_CONFIG.BASE_URL.replace(/\/$/, '');
|
||||
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
return `${baseUrl}${cleanEndpoint}`;
|
||||
}
|
||||
34
susconecta/lib/debug-utils.ts
Normal file
34
susconecta/lib/debug-utils.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Utilitário de debug para requisições HTTP (apenas em desenvolvimento)
|
||||
*/
|
||||
|
||||
export function debugRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
headers: Record<string, string>,
|
||||
body?: any
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
const headersWithoutSensitive = Object.keys(headers).reduce((acc, key) => {
|
||||
// Não logar valores sensíveis, apenas nomes
|
||||
if (key.toLowerCase().includes('apikey') || key.toLowerCase().includes('authorization')) {
|
||||
acc[key] = '[REDACTED]';
|
||||
} else {
|
||||
acc[key] = headers[key];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
const bodyShape = body ? Object.keys(typeof body === 'string' ? JSON.parse(body) : body) : [];
|
||||
|
||||
console.log('[DEBUG] Request Preview:', {
|
||||
method,
|
||||
path: new URL(url).pathname,
|
||||
query: new URL(url).search,
|
||||
headerNames: Object.keys(headers),
|
||||
headers: headersWithoutSensitive,
|
||||
bodyShape,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
83
susconecta/lib/env-config.ts
Normal file
83
susconecta/lib/env-config.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Configuração segura das variáveis de ambiente
|
||||
* Valida se URL e API Key pertencem ao mesmo projeto Supabase
|
||||
*/
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
|
||||
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
||||
|
||||
/**
|
||||
* Extrai o REF do projeto da URL da Supabase
|
||||
*/
|
||||
function extractProjectRef(url: string): string | null {
|
||||
const match = url.match(/https:\/\/([^.]+)\.supabase\.co/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai o REF do projeto da API Key JWT
|
||||
*/
|
||||
function extractProjectRefFromKey(apiKey: string): string | null {
|
||||
try {
|
||||
const payload = JSON.parse(atob(apiKey.split('.')[1]));
|
||||
return payload.ref || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se URL e API Key pertencem ao mesmo projeto
|
||||
*/
|
||||
function validateProjectConsistency(): boolean {
|
||||
const urlRef = extractProjectRef(SUPABASE_URL);
|
||||
const keyRef = extractProjectRefFromKey(SUPABASE_ANON_KEY);
|
||||
|
||||
if (!urlRef || !keyRef) {
|
||||
console.warn('[ENV] Não foi possível extrair REF do projeto');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (urlRef !== keyRef) {
|
||||
console.error('[ENV] ERRO: URL e API Key são de projetos diferentes!', {
|
||||
urlRef,
|
||||
keyRef
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('[ENV] Projeto validado:', urlRef);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validar na inicialização
|
||||
if (typeof window === 'undefined') {
|
||||
// Server-side
|
||||
validateProjectConsistency();
|
||||
} else {
|
||||
// Client-side
|
||||
setTimeout(() => validateProjectConsistency(), 100);
|
||||
}
|
||||
|
||||
export const ENV_CONFIG = {
|
||||
SUPABASE_URL,
|
||||
SUPABASE_ANON_KEY,
|
||||
PROJECT_REF: extractProjectRef(SUPABASE_URL),
|
||||
|
||||
// URLs dos endpoints de autenticação
|
||||
AUTH_ENDPOINTS: {
|
||||
LOGIN: `${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
||||
LOGOUT: `${SUPABASE_URL}/auth/v1/logout`,
|
||||
REFRESH: `${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`,
|
||||
USER: `${SUPABASE_URL}/auth/v1/user`,
|
||||
},
|
||||
|
||||
// Headers padrão
|
||||
DEFAULT_HEADERS: {
|
||||
"Content-Type": "application/json",
|
||||
"apikey": SUPABASE_ANON_KEY,
|
||||
},
|
||||
|
||||
// Validação
|
||||
isValid: validateProjectConsistency(),
|
||||
} as const;
|
||||
260
susconecta/lib/http.ts
Normal file
260
susconecta/lib/http.ts
Normal file
@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Cliente HTTP com refresh automático de token e fila de requisições
|
||||
* Implementa lock para evitar múltiplas chamadas de refresh simultaneamente
|
||||
*/
|
||||
|
||||
import { AUTH_STORAGE_KEYS } from '@/types/auth'
|
||||
import { isExpired } from '@/lib/jwt'
|
||||
import { API_KEY } from '@/lib/config'
|
||||
|
||||
interface QueuedRequest {
|
||||
resolve: (value: any) => void
|
||||
reject: (error: any) => void
|
||||
config: RequestInit & { url: string }
|
||||
}
|
||||
|
||||
class HttpClient {
|
||||
private isRefreshing = false
|
||||
private requestQueue: QueuedRequest[] = []
|
||||
private baseURL: string
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL
|
||||
}
|
||||
|
||||
/**
|
||||
* Processa fila de requisições após refresh bem-sucedido
|
||||
*/
|
||||
private processQueue(error: Error | null, token: string | null = null) {
|
||||
console.log(`[HTTP] Processando fila de ${this.requestQueue.length} requisições`)
|
||||
|
||||
this.requestQueue.forEach(({ resolve, reject, config }) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
// Reexecutar requisição com novo token
|
||||
const headers = {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
resolve(this.executeRequest({ ...config, headers }))
|
||||
}
|
||||
})
|
||||
|
||||
this.requestQueue = []
|
||||
console.log('[HTTP] Fila de requisições processada')
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa refresh de token uma única vez usando lock
|
||||
*/
|
||||
private async refreshToken(): Promise<string | null> {
|
||||
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available')
|
||||
}
|
||||
|
||||
console.log('[HTTP] Iniciando refresh de token...', {
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
|
||||
const response = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=refresh_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': API_KEY // API Key sempre necessária
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('[HTTP] Refresh falhou:', response.status)
|
||||
throw new Error(`Refresh failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Atualizar tokens de forma atômica
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, data.access_token)
|
||||
if (data.refresh_token) {
|
||||
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token)
|
||||
}
|
||||
|
||||
console.log('[HTTP] Token renovado com sucesso!', {
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
return data.access_token
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa requisição HTTP com tratamento de erros
|
||||
*/
|
||||
private async executeRequest(config: RequestInit & { url: string }): Promise<Response> {
|
||||
try {
|
||||
console.log(`[HTTP] Fazendo requisição: ${config.method || 'GET'} ${config.url}`)
|
||||
|
||||
// Delay para visualizar na aba Network
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
const response = await fetch(config.url, config)
|
||||
|
||||
console.log(`[HTTP] Resposta recebida: ${response.status} ${response.statusText}`, {
|
||||
url: config.url,
|
||||
status: response.status,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
|
||||
// Se for 401 e não for uma tentativa de refresh, tentar renovar token
|
||||
if (response.status === 401 && !config.url.includes('/refresh')) {
|
||||
console.log('[HTTP] Status 401 - Verificando possibilidade de refresh token...')
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const token = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
if (token && !isExpired(token)) {
|
||||
// Token ainda é válido, erro pode ser temporário
|
||||
console.log('[HTTP] Token ainda válido - erro pode ser temporário')
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
return response
|
||||
}
|
||||
|
||||
// Token expirado, tentar refresh
|
||||
if (this.isRefreshing) {
|
||||
// Adicionar à fila se já está fazendo refresh
|
||||
return new Promise((resolve, reject) => {
|
||||
this.requestQueue.push({
|
||||
resolve,
|
||||
reject,
|
||||
config
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.isRefreshing = true
|
||||
|
||||
try {
|
||||
const newToken = await this.refreshToken()
|
||||
this.isRefreshing = false
|
||||
|
||||
// Processar fila com sucesso
|
||||
this.processQueue(null, newToken)
|
||||
|
||||
// Reexecutar requisição original
|
||||
const newHeaders = {
|
||||
...config.headers,
|
||||
'apikey': API_KEY, // Garantir API Key
|
||||
Authorization: `Bearer ${newToken}`
|
||||
}
|
||||
|
||||
console.log('[HTTP] Reexecutando requisição com novo token...')
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
return await fetch(config.url, { ...config, headers: newHeaders })
|
||||
} catch (refreshError) {
|
||||
this.isRefreshing = false
|
||||
this.processQueue(refreshError as Error)
|
||||
|
||||
// Logout único em caso de falha no refresh
|
||||
console.error('[HTTP] Refresh FALHOU - fazendo logout automático:', refreshError)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
this.performLogout()
|
||||
|
||||
throw refreshError
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('[HTTP] Erro na requisição:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout único com limpeza de estado
|
||||
*/
|
||||
private performLogout() {
|
||||
// Limpar dados de autenticação
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||
localStorage.removeItem(AUTH_STORAGE_KEYS.USER)
|
||||
|
||||
// Redirecionar para login
|
||||
if (typeof window !== 'undefined') {
|
||||
const userType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) || 'profissional'
|
||||
const loginRoutes = {
|
||||
profissional: '/login',
|
||||
paciente: '/login-paciente',
|
||||
administrador: '/login-admin'
|
||||
}
|
||||
|
||||
const loginRoute = loginRoutes[userType as keyof typeof loginRoutes] || '/login'
|
||||
window.location.href = loginRoute
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Método público para fazer requisições autenticadas
|
||||
*/
|
||||
async request(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const token = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||
|
||||
console.log(`[HTTP] Preparando requisição: ${options.method || 'GET'} ${url}`, {
|
||||
hasToken: !!token,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
})
|
||||
|
||||
const config: RequestInit & { url: string } = {
|
||||
url: url.startsWith('http') ? url : `${this.baseURL}${url}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': API_KEY, // API Key da Supabase sempre presente
|
||||
...(token && { Authorization: `Bearer ${token}` }), // Bearer Token quando usuário logado
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
}
|
||||
|
||||
const response = await this.executeRequest(config)
|
||||
|
||||
console.log(`[HTTP] Requisição finalizada: ${response.status}`, {
|
||||
url: config.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Métodos de conveniência
|
||||
*/
|
||||
async get(url: string, options?: RequestInit): Promise<Response> {
|
||||
return this.request(url, { ...options, method: 'GET' })
|
||||
}
|
||||
|
||||
async post(url: string, data?: any, options?: RequestInit): Promise<Response> {
|
||||
return this.request(url, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined
|
||||
})
|
||||
}
|
||||
|
||||
async put(url: string, data?: any, options?: RequestInit): Promise<Response> {
|
||||
return this.request(url, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined
|
||||
})
|
||||
}
|
||||
|
||||
async delete(url: string, options?: RequestInit): Promise<Response> {
|
||||
return this.request(url, { ...options, method: 'DELETE' })
|
||||
}
|
||||
}
|
||||
|
||||
// Instância única do cliente HTTP
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://mock.apidog.com/m1/1053378-0-default'
|
||||
export const httpClient = new HttpClient(API_BASE_URL)
|
||||
|
||||
export default httpClient
|
||||
133
susconecta/lib/jwt.ts
Normal file
133
susconecta/lib/jwt.ts
Normal file
@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Utilitários JWT com verificação de expiração padronizada
|
||||
* Clock skew tolerance de 60 segundos para compensar diferenças de tempo
|
||||
*/
|
||||
|
||||
interface JWTPayload {
|
||||
exp?: number
|
||||
iat?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const CLOCK_SKEW_SECONDS = 60
|
||||
|
||||
/**
|
||||
* Parse JWT token payload sem validação de assinatura
|
||||
* @param token JWT token
|
||||
* @returns Payload decodificado ou null se inválido
|
||||
*/
|
||||
export function parseJwt(token: string): JWTPayload | null {
|
||||
try {
|
||||
const base64Url = token.split('.')[1]
|
||||
if (!base64Url) return null
|
||||
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
)
|
||||
|
||||
return JSON.parse(jsonPayload)
|
||||
} catch (error) {
|
||||
console.warn('[JWT] Erro ao fazer parse do token:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se token está expirado com tolerância de clock skew
|
||||
* @param token JWT token ou timestamp de expiração em segundos
|
||||
* @returns true se expirado
|
||||
*/
|
||||
export function isExpired(token: string | number): boolean {
|
||||
try {
|
||||
let expTimestamp: number
|
||||
|
||||
if (typeof token === 'string') {
|
||||
const payload = parseJwt(token)
|
||||
if (!payload?.exp) {
|
||||
console.warn('[JWT] Token sem claim exp, considerando válido')
|
||||
return false
|
||||
}
|
||||
expTimestamp = payload.exp
|
||||
} else {
|
||||
expTimestamp = token
|
||||
}
|
||||
|
||||
const nowSeconds = Math.floor(Date.now() / 1000)
|
||||
const isExpiredValue = nowSeconds >= (expTimestamp + CLOCK_SKEW_SECONDS)
|
||||
|
||||
console.log('[JWT] Verificação de expiração:', {
|
||||
nowSeconds,
|
||||
expTimestamp,
|
||||
clockSkew: CLOCK_SKEW_SECONDS,
|
||||
isExpired: isExpiredValue,
|
||||
timeUntilExpiry: expTimestamp - nowSeconds
|
||||
})
|
||||
|
||||
return isExpiredValue
|
||||
} catch (error) {
|
||||
console.warn('[JWT] Erro na verificação de expiração:', error)
|
||||
return true // Assumir expirado em caso de erro
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se token deve ser renovado (expira em menos de 5 minutos)
|
||||
* @param token JWT token ou timestamp de expiração em segundos
|
||||
* @returns true se deve renovar
|
||||
*/
|
||||
export function shouldRefresh(token: string | number): boolean {
|
||||
try {
|
||||
let expTimestamp: number
|
||||
|
||||
if (typeof token === 'string') {
|
||||
const payload = parseJwt(token)
|
||||
if (!payload?.exp) return false
|
||||
expTimestamp = payload.exp
|
||||
} else {
|
||||
expTimestamp = token
|
||||
}
|
||||
|
||||
const nowSeconds = Math.floor(Date.now() / 1000)
|
||||
const refreshThreshold = 5 * 60 // 5 minutos
|
||||
const shouldRefreshValue = nowSeconds >= (expTimestamp - refreshThreshold)
|
||||
|
||||
console.log('[JWT] Verificação de renovação:', {
|
||||
nowSeconds,
|
||||
expTimestamp,
|
||||
refreshThreshold,
|
||||
shouldRefresh: shouldRefreshValue,
|
||||
timeUntilRefresh: expTimestamp - refreshThreshold - nowSeconds
|
||||
})
|
||||
|
||||
return shouldRefreshValue
|
||||
} catch (error) {
|
||||
console.warn('[JWT] Erro na verificação de renovação:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai informações úteis do token
|
||||
* @param token JWT token
|
||||
* @returns Informações do token ou null
|
||||
*/
|
||||
export function getTokenInfo(token: string): {
|
||||
payload: JWTPayload
|
||||
isExpired: boolean
|
||||
shouldRefresh: boolean
|
||||
expiresAt: Date | null
|
||||
} | null {
|
||||
const payload = parseJwt(token)
|
||||
if (!payload) return null
|
||||
|
||||
return {
|
||||
payload,
|
||||
isExpired: isExpired(token),
|
||||
shouldRefresh: shouldRefresh(token),
|
||||
expiresAt: payload.exp ? new Date(payload.exp * 1000) : null
|
||||
}
|
||||
}
|
||||
84
susconecta/lib/simple-auth.ts
Normal file
84
susconecta/lib/simple-auth.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Versão simplificada para testar conexão com Supabase
|
||||
*/
|
||||
|
||||
export async function testSupabaseConnection() {
|
||||
const url = 'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/';
|
||||
const headers = {
|
||||
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
console.log('[TEST] Testando conexão com Supabase...');
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
console.log('[TEST] Status:', response.status);
|
||||
console.log('[TEST] Headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (response.ok) {
|
||||
console.log('[TEST] ✅ Conexão com Supabase OK!');
|
||||
} else {
|
||||
console.log('[TEST] ❌ Problema na conexão:', response.statusText);
|
||||
}
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('[TEST] Erro na conexão:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versão simplificada do login para debug
|
||||
*/
|
||||
export async function simpleLogin(email: string, password: string) {
|
||||
const url = 'https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password';
|
||||
|
||||
const payload = {
|
||||
email: email,
|
||||
password: password
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ',
|
||||
};
|
||||
|
||||
console.log('[SIMPLE-LOGIN] Tentando login simples...', {
|
||||
url,
|
||||
email,
|
||||
headers: Object.keys(headers)
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
console.log('[SIMPLE-LOGIN] Response:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseText,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return JSON.parse(responseText);
|
||||
} else {
|
||||
throw new Error(`Login failed: ${response.status} - ${responseText}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SIMPLE-LOGIN] Erro:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
406
susconecta/package-lock.json
generated
406
susconecta/package-lock.json
generated
@ -2186,6 +2186,22 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/quill": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
|
||||
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parchment": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
|
||||
@ -2207,6 +2223,19 @@
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/signature_pad": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/signature_pad/-/signature_pad-2.3.6.tgz",
|
||||
"integrity": "sha512-v3j92gCQJoxomHhd+yaG4Vsf8tRS/XbzWKqDv85UsqjMGy4zhokuwKe4b6vhbgncKkh+thF+gpz6+fypTtnFqQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
@ -2336,53 +2365,6 @@
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"es-define-property": "^1.0.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001739",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
|
||||
@ -2677,40 +2659,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
@ -2727,6 +2675,16 @@
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.213",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz",
|
||||
@ -2775,36 +2733,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.10",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
||||
@ -2830,6 +2758,35 @@
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@ -2843,24 +2800,6 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/geist": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/geist/-/geist-1.4.2.tgz",
|
||||
@ -2870,30 +2809,6 @@
|
||||
"next": ">=13.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
@ -2903,37 +2818,26 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||
@ -2963,6 +2867,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
@ -3272,15 +3182,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
@ -3441,6 +3342,35 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parchment": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
|
||||
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -3491,6 +3421,69 @@
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/quill": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
|
||||
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"clone": "^2.1.1",
|
||||
"deep-equal": "^1.0.1",
|
||||
"eventemitter3": "^2.0.3",
|
||||
"extend": "^3.0.2",
|
||||
"parchment": "^1.1.4",
|
||||
"quill-delta": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-delta": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
|
||||
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deep-equal": "^1.0.1",
|
||||
"extend": "^3.0.2",
|
||||
"fast-diff": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/quill/node_modules/eventemitter3": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@ -3749,6 +3742,13 @@
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
@ -3780,38 +3780,6 @@
|
||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
|
||||
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"functions-have-names": "^1.2.3",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/signature_pad": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-2.3.2.tgz",
|
||||
|
||||
90
susconecta/types/auth.ts
Normal file
90
susconecta/types/auth.ts
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Tipos estritos para autenticação sem any
|
||||
*/
|
||||
|
||||
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated'
|
||||
|
||||
export type UserType = 'profissional' | 'paciente' | 'administrador'
|
||||
|
||||
export interface UserData {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
userType: UserType
|
||||
profile?: {
|
||||
cpf?: string
|
||||
crm?: string // Para profissionais
|
||||
telefone?: string
|
||||
foto_url?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
user: UserData
|
||||
}
|
||||
|
||||
export interface RefreshTokenResponse {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
export interface AuthError {
|
||||
message: string
|
||||
code: string
|
||||
details?: unknown
|
||||
}
|
||||
|
||||
export interface AuthContextType {
|
||||
authStatus: AuthStatus
|
||||
user: UserData | null
|
||||
token: string | null
|
||||
login: (email: string, password: string, userType: UserType) => Promise<boolean>
|
||||
logout: () => Promise<void>
|
||||
refreshToken: () => Promise<boolean>
|
||||
}
|
||||
|
||||
export interface AuthStorageKeys {
|
||||
readonly TOKEN: string
|
||||
readonly REFRESH_TOKEN: string
|
||||
readonly USER: string
|
||||
readonly USER_TYPE: string
|
||||
}
|
||||
|
||||
export type UserTypeRoutes = {
|
||||
readonly [K in UserType]: string
|
||||
}
|
||||
|
||||
export type LoginRoutes = {
|
||||
readonly [K in UserType]: string
|
||||
}
|
||||
|
||||
// Constantes para localStorage
|
||||
export const AUTH_STORAGE_KEYS: AuthStorageKeys = {
|
||||
TOKEN: 'auth_token',
|
||||
REFRESH_TOKEN: 'auth_refresh_token',
|
||||
USER: 'auth_user',
|
||||
USER_TYPE: 'auth_user_type',
|
||||
} as const
|
||||
|
||||
// Rotas baseadas no tipo de usuário
|
||||
export const USER_TYPE_ROUTES: UserTypeRoutes = {
|
||||
profissional: '/profissional',
|
||||
paciente: '/paciente',
|
||||
administrador: '/dashboard',
|
||||
} as const
|
||||
|
||||
export const LOGIN_ROUTES: LoginRoutes = {
|
||||
profissional: '/login',
|
||||
paciente: '/login-paciente',
|
||||
administrador: '/login-admin',
|
||||
} as const
|
||||
Loading…
x
Reference in New Issue
Block a user