integrando os endpoints de login e logout #24
@ -9,8 +9,10 @@ export default function MainRoutesLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
console.log('[MAIN-ROUTES-LAYOUT] Layout do administrador carregado')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredUserType="administrador">
|
<ProtectedRoute requiredUserType={["administrador"]}>
|
||||||
<div className="min-h-screen bg-background flex">
|
<div className="min-h-screen bg-background flex">
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
import { AuthenticationError } from '@/lib/auth'
|
||||||
|
|
||||||
export default function LoginAdminPage() {
|
export default function LoginAdminPage() {
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||||
@ -20,29 +21,27 @@ export default function LoginAdminPage() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
// Simular delay de autenticação
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
// Tentar fazer login usando o contexto com tipo administrador
|
||||||
|
const success = await login(credentials.email, credentials.password, 'administrador')
|
||||||
|
|
||||||
// Tentar fazer login usando o contexto com tipo administrador
|
if (success) {
|
||||||
const success = login(credentials.email, credentials.password, 'administrador')
|
console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...')
|
||||||
|
|
||||||
if (success) {
|
// Redirecionamento direto - solução que funcionou
|
||||||
// Redirecionar para o dashboard do administrador
|
window.location.href = '/dashboard'
|
||||||
setTimeout(() => {
|
}
|
||||||
router.push('/dashboard')
|
} catch (err) {
|
||||||
|
console.error('[LOGIN-ADMIN] Erro no login:', err)
|
||||||
|
|
||||||
// Fallback: usar window.location se router.push não funcionar
|
if (err instanceof AuthenticationError) {
|
||||||
setTimeout(() => {
|
setError(err.message)
|
||||||
if (window.location.pathname === '/login-admin') {
|
} else {
|
||||||
window.location.href = '/dashboard'
|
setError('Erro inesperado. Tente novamente.')
|
||||||
}
|
}
|
||||||
}, 100)
|
} finally {
|
||||||
}, 100)
|
setLoading(false)
|
||||||
} else {
|
|
||||||
setError('Email ou senha incorretos')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -75,6 +74,7 @@ export default function LoginAdminPage() {
|
|||||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||||
required
|
required
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -90,6 +90,7 @@ export default function LoginAdminPage() {
|
|||||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||||
required
|
required
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
import { AuthenticationError } from '@/lib/auth'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||||
@ -20,29 +21,27 @@ export default function LoginPage() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
// Simular delay de autenticação
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
// Tentar fazer login usando o contexto com tipo profissional
|
||||||
|
const success = await login(credentials.email, credentials.password, 'profissional')
|
||||||
|
|
||||||
// Tentar fazer login usando o contexto com tipo profissional
|
if (success) {
|
||||||
const success = login(credentials.email, credentials.password, 'profissional')
|
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
|
||||||
|
|
||||||
if (success) {
|
// Redirecionamento direto - solução que funcionou
|
||||||
// Redirecionar para a página do profissional
|
window.location.href = '/profissional'
|
||||||
setTimeout(() => {
|
}
|
||||||
router.push('/profissional')
|
} catch (err) {
|
||||||
|
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
|
||||||
|
|
||||||
// Fallback: usar window.location se router.push não funcionar
|
if (err instanceof AuthenticationError) {
|
||||||
setTimeout(() => {
|
setError(err.message)
|
||||||
if (window.location.pathname === '/login') {
|
} else {
|
||||||
window.location.href = '/profissional'
|
setError('Erro inesperado. Tente novamente.')
|
||||||
}
|
}
|
||||||
}, 100)
|
} finally {
|
||||||
}, 100)
|
setLoading(false)
|
||||||
} else {
|
|
||||||
setError('Email ou senha incorretos')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -75,6 +74,7 @@ export default function LoginPage() {
|
|||||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
||||||
required
|
required
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -90,6 +90,7 @@ export default function LoginPage() {
|
|||||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||||
required
|
required
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
@ -1,43 +1,137 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
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 {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
requiredUserType?: string
|
requiredUserType?: UserType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProtectedRoute({ children, requiredUserType }: ProtectedRouteProps) {
|
export default function ProtectedRoute({
|
||||||
const { isAuthenticated, userType, checkAuth } = useAuth()
|
children,
|
||||||
|
requiredUserType
|
||||||
|
}: ProtectedRouteProps) {
|
||||||
|
const { authStatus, user } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const isRedirecting = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAuth()
|
// Evitar múltiplos redirects
|
||||||
}, [checkAuth])
|
if (isRedirecting.current) return
|
||||||
|
|
||||||
useEffect(() => {
|
// Durante loading, não fazer nada
|
||||||
if (!isAuthenticated) {
|
if (authStatus === 'loading') return
|
||||||
console.log('Usuário não autenticado, redirecionando para login...')
|
|
||||||
router.push('/login')
|
// Se não autenticado, redirecionar para login
|
||||||
} else if (requiredUserType && userType !== requiredUserType) {
|
if (authStatus === 'unauthenticated') {
|
||||||
console.log(`Tipo de usuário incorreto. Esperado: ${requiredUserType}, Atual: ${userType}`)
|
isRedirecting.current = true
|
||||||
router.push('/login')
|
|
||||||
} else {
|
console.log('[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...')
|
||||||
console.log('Usuário autenticado!')
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
<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>
|
||||||
</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}</>
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ import { useState, useEffect, useRef } from "react"
|
|||||||
import { SidebarTrigger } from "../ui/sidebar"
|
import { SidebarTrigger } from "../ui/sidebar"
|
||||||
|
|
||||||
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
||||||
const { logout, userEmail, userType } = useAuth();
|
const { logout, user } = useAuth();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
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="p-4 border-b border-gray-100">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-sm font-semibold leading-none">
|
<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>
|
</p>
|
||||||
{userEmail ? (
|
{user?.email ? (
|
||||||
<p className="text-xs leading-none text-gray-600">{userEmail}</p>
|
<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-gray-600">Email não disponível</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs leading-none text-blue-600 font-medium">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -95,15 +95,8 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
|
|
||||||
// Logout específico para administrador
|
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
|
||||||
if (userType === 'administrador') {
|
logout();
|
||||||
localStorage.removeItem('isAuthenticated');
|
|
||||||
localStorage.removeItem('userEmail');
|
|
||||||
localStorage.removeItem('userType');
|
|
||||||
window.location.href = '/login-admin';
|
|
||||||
} else {
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 cursor-pointer"
|
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
|
<Button
|
||||||
variant="outline"
|
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"
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
Sou Paciente
|
<Link href="/login-paciente">Sou Paciente</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
||||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
<Link href="/login">Sou Profissional de Saúde</Link>
|
||||||
@ -94,8 +95,9 @@ export function Header() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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"
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
Sou Paciente
|
<Link href="/login-paciente">Sou Paciente</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full">
|
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full">
|
||||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
<Link href="/login">Sou Profissional de Saúde</Link>
|
||||||
|
|||||||
@ -4,20 +4,20 @@ import Link from "next/link"
|
|||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection() {
|
||||||
return (
|
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="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-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
||||||
APROXIMANDO MÉDICOS E PACIENTES
|
APROXIMANDO MÉDICOS E PACIENTES
|
||||||
</div>
|
</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{" "}
|
Segurança, <span className="text-primary">Confiabilidade</span> e{" "}
|
||||||
<span className="text-primary">Rapidez</span>
|
<span className="text-primary">Rapidez</span>
|
||||||
</h1>
|
</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>Experimente o futuro dos agendamentos.</p>
|
||||||
<p>Encontre profissionais capacitados e marque já sua consulta.</p>
|
<p>Encontre profissionais capacitados e marque já sua consulta.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -25,33 +25,38 @@ export function HeroSection() {
|
|||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<Button size="lg" className="bg-primary hover:bg-primary/90 text-primary-foreground">
|
<Button
|
||||||
Sou Paciente
|
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>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
<div className="relative">
|
<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
|
<img
|
||||||
src="/medico-sorridente-de-tiro-medio-vestindo-casaco.jpg"
|
src="/medico-sorridente-de-tiro-medio-vestindo-casaco.jpg"
|
||||||
alt="Médico profissional sorrindo"
|
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>
|
||||||
</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 items-start space-x-3">
|
||||||
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
<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" />
|
<Shield className="w-4 h-4 text-primary" />
|
||||||
|
|||||||
@ -1,100 +1,243 @@
|
|||||||
'use client'
|
'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'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
|
||||||
interface AuthContextType {
|
import { isExpired, parseJwt } from '@/lib/jwt'
|
||||||
isAuthenticated: boolean
|
import { httpClient } from '@/lib/http'
|
||||||
userEmail: string | null
|
import type {
|
||||||
userType: string | null
|
AuthContextType,
|
||||||
login: (email: string, password: string, userType: string) => boolean
|
UserData,
|
||||||
logout: () => void
|
AuthStatus,
|
||||||
checkAuth: () => void
|
UserType
|
||||||
}
|
} from '@/types/auth'
|
||||||
|
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth'
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [authStatus, setAuthStatus] = useState<AuthStatus>('loading')
|
||||||
const [userEmail, setUserEmail] = useState<string | null>(null)
|
const [user, setUser] = useState<UserData | null>(null)
|
||||||
const [userType, setUserType] = useState<string | null>(null)
|
const [token, setToken] = useState<string | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
|
||||||
const checkAuth = () => {
|
// Utilitários de armazenamento memorizados
|
||||||
|
const clearAuthData = useCallback(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const auth = localStorage.getItem('isAuthenticated')
|
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN)
|
||||||
const email = localStorage.getItem('userEmail')
|
localStorage.removeItem(AUTH_STORAGE_KEYS.USER)
|
||||||
const type = localStorage.getItem('userType')
|
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||||
|
// Manter USER_TYPE para redirecionamento correto
|
||||||
if (auth === 'true' && email) {
|
|
||||||
setIsAuthenticated(true)
|
|
||||||
setUserEmail(email)
|
|
||||||
setUserType(type)
|
|
||||||
} else {
|
|
||||||
setIsAuthenticated(false)
|
|
||||||
setUserEmail(null)
|
|
||||||
setUserType(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setIsLoading(false)
|
setUser(null)
|
||||||
}
|
setToken(null)
|
||||||
|
setAuthStatus('unauthenticated')
|
||||||
useEffect(() => {
|
console.log('[AUTH] Dados de autenticação limpos - logout realizado')
|
||||||
checkAuth()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const login = (email: string, password: string, userType: string): boolean => {
|
const saveAuthData = useCallback((
|
||||||
if (email === 'teste@gmail.com' && password === '123456') {
|
accessToken: string,
|
||||||
localStorage.setItem('isAuthenticated', 'true')
|
userData: UserData,
|
||||||
localStorage.setItem('userEmail', email)
|
refreshToken?: string
|
||||||
localStorage.setItem('userType', userType)
|
) => {
|
||||||
setIsAuthenticated(true)
|
try {
|
||||||
setUserEmail(email)
|
if (typeof window !== 'undefined') {
|
||||||
setUserType(userType)
|
// 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
|
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
|
return false
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const logout = () => {
|
// Getters memorizados
|
||||||
// Usar o estado atual em vez do localStorage para evitar condição de corrida
|
const contextValue = useMemo(() => ({
|
||||||
const currentUserType = userType || localStorage.getItem('userType')
|
authStatus,
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
refreshToken
|
||||||
|
}), [authStatus, user, token, login, logout, refreshToken])
|
||||||
|
|
||||||
localStorage.removeItem('isAuthenticated')
|
// Inicialização única
|
||||||
localStorage.removeItem('userEmail')
|
useEffect(() => {
|
||||||
localStorage.removeItem('userType')
|
if (!hasInitialized.current && typeof window !== 'undefined') {
|
||||||
setIsAuthenticated(false)
|
hasInitialized.current = true
|
||||||
setUserEmail(null)
|
checkAuth()
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
}
|
}, [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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{
|
<AuthContext.Provider value={contextValue}>
|
||||||
isAuthenticated,
|
|
||||||
userEmail,
|
|
||||||
userType,
|
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
checkAuth
|
|
||||||
}}>
|
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -82,10 +82,24 @@ export const PATHS = {
|
|||||||
} as const;
|
} 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> {
|
function headers(kind: "json" | "form" = "json"): Record<string, string> {
|
||||||
const h: 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";
|
if (kind === "json") h["Content-Type"] = "application/json";
|
||||||
return h;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
406
susconecta/package-lock.json
generated
406
susconecta/package-lock.json
generated
@ -2186,6 +2186,22 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.24",
|
"version": "18.3.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
|
||||||
@ -2207,6 +2223,19 @@
|
|||||||
"@types/react": "^18.0.0"
|
"@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": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
"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": ">=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": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001739",
|
"version": "1.0.30001739",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
|
||||||
@ -2677,40 +2659,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
@ -2727,6 +2675,16 @@
|
|||||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.213",
|
"version": "1.5.213",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz",
|
||||||
@ -2775,36 +2733,6 @@
|
|||||||
"node": ">=10.13.0"
|
"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": {
|
"node_modules/es-toolkit": {
|
||||||
"version": "1.39.10",
|
"version": "1.39.10",
|
||||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
||||||
@ -2830,6 +2758,35 @@
|
|||||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
@ -2843,24 +2800,6 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"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": {
|
"node_modules/geist": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/geist/-/geist-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/geist/-/geist-1.4.2.tgz",
|
||||||
@ -2870,30 +2809,6 @@
|
|||||||
"next": ">=13.2.0"
|
"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": {
|
"node_modules/get-nonce": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
@ -2903,37 +2818,26 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/immer": {
|
||||||
"version": "10.1.3",
|
"version": "10.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||||
@ -2963,6 +2867,12 @@
|
|||||||
"node": ">=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": {
|
"node_modules/jiti": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||||
@ -3272,15 +3182,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@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": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@ -3441,6 +3342,35 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@ -3491,6 +3421,69 @@
|
|||||||
"url": "https://opencollective.com/preact"
|
"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": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
@ -3749,6 +3742,13 @@
|
|||||||
"redux": "^5.0.0"
|
"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": {
|
"node_modules/reselect": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
@ -3780,38 +3780,6 @@
|
|||||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/signature_pad": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-2.3.2.tgz",
|
"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