integrando os endpoints de login e logout #24

Merged
Jonasbomfim merged 2 commits from feature/add-authentication-api into develop 2025-09-28 18:55:02 +00:00
20 changed files with 3161 additions and 452 deletions

View File

@ -9,8 +9,10 @@ export default function MainRoutesLayout({
}: {
children: React.ReactNode;
}) {
console.log('[MAIN-ROUTES-LAYOUT] Layout do administrador carregado')
return (
<ProtectedRoute requiredUserType="administrador">
<ProtectedRoute requiredUserType={["administrador"]}>
<div className="min-h-screen bg-background flex">
<SidebarProvider>
<Sidebar />

View File

@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AuthenticationError } from '@/lib/auth'
export default function LoginAdminPage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
@ -20,29 +21,27 @@ export default function LoginAdminPage() {
setLoading(true)
setError('')
// Simular delay de autenticação
await new Promise(resolve => setTimeout(resolve, 1000))
try {
// 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
const success = login(credentials.email, credentials.password, 'administrador')
if (success) {
console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...')
if (success) {
// Redirecionar para o dashboard do administrador
setTimeout(() => {
router.push('/dashboard')
// Redirecionamento direto - solução que funcionou
window.location.href = '/dashboard'
}
} catch (err) {
console.error('[LOGIN-ADMIN] Erro no login:', err)
// Fallback: usar window.location se router.push não funcionar
setTimeout(() => {
if (window.location.pathname === '/login-admin') {
window.location.href = '/dashboard'
}
}, 100)
}, 100)
} else {
setError('Email ou senha incorretos')
if (err instanceof AuthenticationError) {
setError(err.message)
} else {
setError('Erro inesperado. Tente novamente.')
}
} finally {
setLoading(false)
}
setLoading(false)
}
return (
@ -75,6 +74,7 @@ export default function LoginAdminPage() {
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>
@ -90,6 +90,7 @@ export default function LoginAdminPage() {
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>

View 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>
)
}

View File

@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AuthenticationError } from '@/lib/auth'
export default function LoginPage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
@ -20,29 +21,27 @@ export default function LoginPage() {
setLoading(true)
setError('')
// Simular delay de autenticação
await new Promise(resolve => setTimeout(resolve, 1000))
try {
// 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
const success = login(credentials.email, credentials.password, 'profissional')
if (success) {
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
if (success) {
// Redirecionar para a página do profissional
setTimeout(() => {
router.push('/profissional')
// Redirecionamento direto - solução que funcionou
window.location.href = '/profissional'
}
} catch (err) {
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
// Fallback: usar window.location se router.push não funcionar
setTimeout(() => {
if (window.location.pathname === '/login') {
window.location.href = '/profissional'
}
}, 100)
}, 100)
} else {
setError('Email ou senha incorretos')
if (err instanceof AuthenticationError) {
setError(err.message)
} else {
setError('Erro inesperado. Tente novamente.')
}
} finally {
setLoading(false)
}
setLoading(false)
}
return (
@ -75,6 +74,7 @@ export default function LoginPage() {
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>
@ -90,6 +90,7 @@ export default function LoginPage() {
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
required
className="mt-1"
disabled={loading}
/>
</div>

View 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

View File

@ -1,43 +1,137 @@
'use client'
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/hooks/useAuth'
import type { UserType } from '@/types/auth'
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
interface ProtectedRouteProps {
children: React.ReactNode
requiredUserType?: string
requiredUserType?: UserType[]
}
export default function ProtectedRoute({ children, requiredUserType }: ProtectedRouteProps) {
const { isAuthenticated, userType, checkAuth } = useAuth()
export default function ProtectedRoute({
children,
requiredUserType
}: ProtectedRouteProps) {
const { authStatus, user } = useAuth()
const router = useRouter()
const isRedirecting = useRef(false)
useEffect(() => {
checkAuth()
}, [checkAuth])
// Evitar múltiplos redirects
if (isRedirecting.current) return
useEffect(() => {
if (!isAuthenticated) {
console.log('Usuário não autenticado, redirecionando para login...')
router.push('/login')
} else if (requiredUserType && userType !== requiredUserType) {
console.log(`Tipo de usuário incorreto. Esperado: ${requiredUserType}, Atual: ${userType}`)
router.push('/login')
} else {
console.log('Usuário autenticado!')
// Durante loading, não fazer nada
if (authStatus === 'loading') return
// Se não autenticado, redirecionar para login
if (authStatus === 'unauthenticated') {
isRedirecting.current = true
console.log('[PROTECTED-ROUTE] Usuário NÃO autenticado - redirecionando...')
// Determinar página de login baseada no histórico
let userType: UserType = 'profissional'
if (typeof window !== 'undefined') {
try {
const storedUserType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
if (storedUserType && ['profissional', 'paciente', 'administrador'].includes(storedUserType)) {
userType = storedUserType as UserType
}
} catch (error) {
console.warn('[PROTECTED-ROUTE] Erro ao ler localStorage:', error)
}
}
const loginRoute = LOGIN_ROUTES[userType]
console.log('[PROTECTED-ROUTE] Redirecionando para login:', {
userType,
loginRoute,
timestamp: new Date().toLocaleTimeString()
})
router.push(loginRoute)
return
}
}, [isAuthenticated, userType, requiredUserType, router])
if (!isAuthenticated || (requiredUserType && userType !== requiredUserType)) {
// Se autenticado mas não tem permissão para esta página
if (authStatus === 'authenticated' && user && requiredUserType && !requiredUserType.includes(user.userType)) {
isRedirecting.current = true
console.log('[PROTECTED-ROUTE] Usuário SEM permissão para esta página', {
userType: user.userType,
requiredTypes: requiredUserType
})
const correctRoute = USER_TYPE_ROUTES[user.userType]
console.log('[PROTECTED-ROUTE] Redirecionando para área correta:', correctRoute)
router.push(correctRoute)
return
}
// Se chegou aqui, acesso está autorizado
if (authStatus === 'authenticated') {
console.log('[PROTECTED-ROUTE] ACESSO AUTORIZADO!', {
userType: user?.userType,
email: user?.email,
timestamp: new Date().toLocaleTimeString()
})
isRedirecting.current = false
}
}, [authStatus, user, requiredUserType, router])
// Durante loading, mostrar spinner
if (authStatus === 'loading') {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Redirecionando para login...</p>
<p className="mt-4 text-gray-600">Verificando autenticação...</p>
</div>
</div>
)
}
// Se não autenticado ou redirecionando, mostrar spinner
if (authStatus === 'unauthenticated' || isRedirecting.current) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Redirecionando...</p>
</div>
</div>
)
}
// Se usuário não tem permissão, mostrar fallback (não deveria chegar aqui devido ao useEffect)
if (requiredUserType && user && !requiredUserType.includes(user.userType)) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Acesso Negado</h2>
<p className="text-gray-600 mb-4">
Você não tem permissão para acessar esta página.
</p>
<p className="text-sm text-gray-500 mb-6">
Tipo de acesso necessário: {requiredUserType.join(' ou ')}
<br />
Seu tipo de acesso: {user.userType}
</p>
<button
onClick={() => router.push(USER_TYPE_ROUTES[user.userType])}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 cursor-pointer"
>
Ir para minha área
</button>
</div>
</div>
)
}
// Finalmente, renderizar conteúdo protegido
return <>{children}</>
}

View File

@ -9,7 +9,7 @@ import { useState, useEffect, useRef } from "react"
import { SidebarTrigger } from "../ui/sidebar"
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
const { logout, userEmail, userType } = useAuth();
const { logout, user } = useAuth();
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
@ -69,15 +69,15 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
<div className="p-4 border-b border-gray-100">
<div className="flex flex-col space-y-1">
<p className="text-sm font-semibold leading-none">
{userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'}
{user?.userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'}
</p>
{userEmail ? (
<p className="text-xs leading-none text-gray-600">{userEmail}</p>
{user?.email ? (
<p className="text-xs leading-none text-gray-600">{user.email}</p>
) : (
<p className="text-xs leading-none text-gray-600">Email não disponível</p>
)}
<p className="text-xs leading-none text-blue-600 font-medium">
Tipo: {userType === 'administrador' ? 'Administrador' : userType || 'Não definido'}
Tipo: {user?.userType === 'administrador' ? 'Administrador' : user?.userType || 'Não definido'}
</p>
</div>
</div>
@ -95,15 +95,8 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
e.preventDefault();
setDropdownOpen(false);
// Logout específico para administrador
if (userType === 'administrador') {
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userEmail');
localStorage.removeItem('userType');
window.location.href = '/login-admin';
} else {
logout();
}
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
logout();
}}
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 cursor-pointer"
>

View File

@ -46,8 +46,9 @@ export function Header() {
<Button
variant="outline"
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
asChild
>
Sou Paciente
<Link href="/login-paciente">Sou Paciente</Link>
</Button>
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground">
<Link href="/login">Sou Profissional de Saúde</Link>
@ -94,8 +95,9 @@ export function Header() {
<Button
variant="outline"
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
asChild
>
Sou Paciente
<Link href="/login-paciente">Sou Paciente</Link>
</Button>
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full">
<Link href="/login">Sou Profissional de Saúde</Link>

View File

@ -4,20 +4,20 @@ import Link from "next/link"
export function HeroSection() {
return (
<section className="py-16 lg:py-24 bg-background">
<section className="py-8 lg:py-12 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12 items-center">
<div className="grid lg:grid-cols-2 gap-8 items-center">
{}
<div className="space-y-8">
<div className="space-y-4">
<div className="space-y-6">
<div className="space-y-3">
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
APROXIMANDO MÉDICOS E PACIENTES
</div>
<h1 className="text-4xl lg:text-5xl font-bold text-foreground leading-tight text-balance">
<h1 className="text-3xl lg:text-4xl font-bold text-foreground leading-tight text-balance">
Segurança, <span className="text-primary">Confiabilidade</span> e{" "}
<span className="text-primary">Rapidez</span>
</h1>
<div className="space-y-2 text-lg text-muted-foreground">
<div className="space-y-1 text-base text-muted-foreground">
<p>Experimente o futuro dos agendamentos.</p>
<p>Encontre profissionais capacitados e marque sua consulta.</p>
</div>
@ -25,33 +25,38 @@ export function HeroSection() {
{}
<div className="flex flex-col sm:flex-row gap-4">
<Button size="lg" className="bg-primary hover:bg-primary/90 text-primary-foreground">
Sou Paciente
<Button
size="lg"
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer"
asChild
>
<Link href="/login-paciente">Portal do Paciente</Link>
</Button>
<Button
size="lg"
variant="outline"
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent"
className="text-primary border-primary hover:bg-primary hover:text-primary-foreground bg-transparent cursor-pointer"
asChild
>
<Link href="/profissional">Sou Profissional de Saúde</Link>
<Link href="/login">Sou Profissional de Saúde</Link>
</Button>
</div>
</div>
{}
<div className="relative">
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-8">
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-6">
<img
src="/medico-sorridente-de-tiro-medio-vestindo-casaco.jpg"
alt="Médico profissional sorrindo"
className="w-full h-auto rounded-lg"
className="w-full h-auto rounded-lg min-h-80 max-h-[500px] object-cover object-center"
/>
</div>
</div>
</div>
{}
<div className="mt-16 grid md:grid-cols-3 gap-8">
<div className="mt-10 grid md:grid-cols-3 gap-6">
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
<Shield className="w-4 h-4 text-primary" />

View File

@ -1,100 +1,243 @@
'use client'
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
import { useRouter } from 'next/navigation'
interface AuthContextType {
isAuthenticated: boolean
userEmail: string | null
userType: string | null
login: (email: string, password: string, userType: string) => boolean
logout: () => void
checkAuth: () => void
}
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
import { isExpired, parseJwt } from '@/lib/jwt'
import { httpClient } from '@/lib/http'
import type {
AuthContextType,
UserData,
AuthStatus,
UserType
} from '@/types/auth'
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth'
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [userEmail, setUserEmail] = useState<string | null>(null)
const [userType, setUserType] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [authStatus, setAuthStatus] = useState<AuthStatus>('loading')
const [user, setUser] = useState<UserData | null>(null)
const [token, setToken] = useState<string | null>(null)
const router = useRouter()
const hasInitialized = useRef(false)
const checkAuth = () => {
// Utilitários de armazenamento memorizados
const clearAuthData = useCallback(() => {
if (typeof window !== 'undefined') {
const auth = localStorage.getItem('isAuthenticated')
const email = localStorage.getItem('userEmail')
const type = localStorage.getItem('userType')
if (auth === 'true' && email) {
setIsAuthenticated(true)
setUserEmail(email)
setUserType(type)
} else {
setIsAuthenticated(false)
setUserEmail(null)
setUserType(null)
}
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN)
localStorage.removeItem(AUTH_STORAGE_KEYS.USER)
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
// Manter USER_TYPE para redirecionamento correto
}
setIsLoading(false)
}
useEffect(() => {
checkAuth()
setUser(null)
setToken(null)
setAuthStatus('unauthenticated')
console.log('[AUTH] Dados de autenticação limpos - logout realizado')
}, [])
const login = (email: string, password: string, userType: string): boolean => {
if (email === 'teste@gmail.com' && password === '123456') {
localStorage.setItem('isAuthenticated', 'true')
localStorage.setItem('userEmail', email)
localStorage.setItem('userType', userType)
setIsAuthenticated(true)
setUserEmail(email)
setUserType(userType)
const saveAuthData = useCallback((
accessToken: string,
userData: UserData,
refreshToken?: string
) => {
try {
if (typeof window !== 'undefined') {
// Persistir dados de forma atômica
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken)
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType)
if (refreshToken) {
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
}
}
setToken(accessToken)
setUser(userData)
setAuthStatus('authenticated')
console.log('[AUTH] LOGIN realizado - Dados salvos!', {
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString()
})
} catch (error) {
console.error('[AUTH] Erro ao salvar dados:', error)
clearAuthData()
}
}, [clearAuthData])
// Verificação inicial de autenticação
const checkAuth = useCallback(async (): Promise<void> => {
if (typeof window === 'undefined') {
setAuthStatus('unauthenticated')
return
}
try {
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER)
console.log('[AUTH] Verificando sessão...', {
hasToken: !!storedToken,
hasUser: !!storedUser,
timestamp: new Date().toLocaleTimeString()
})
// Pequeno delay para visualizar logs
await new Promise(resolve => setTimeout(resolve, 800))
if (!storedToken || !storedUser) {
console.log('[AUTH] Dados ausentes - sessão inválida')
await new Promise(resolve => setTimeout(resolve, 500))
clearAuthData()
return
}
// Verificar se token está expirado
if (isExpired(storedToken)) {
console.log('[AUTH] Token expirado - tentando renovar...')
await new Promise(resolve => setTimeout(resolve, 1000))
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
if (refreshToken && !isExpired(refreshToken)) {
// Tentar renovar via HTTP client (que já tem a lógica)
try {
await httpClient.get('/auth/v1/me') // Trigger refresh se necessário
// Se chegou aqui, refresh foi bem-sucedido
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
const userData = JSON.parse(storedUser) as UserData
if (newToken && newToken !== storedToken) {
setToken(newToken)
setUser(userData)
setAuthStatus('authenticated')
console.log('[AUTH] Token RENOVADO automaticamente!')
await new Promise(resolve => setTimeout(resolve, 800))
return
}
} catch (refreshError) {
console.log('❌ [AUTH] Falha no refresh automático')
await new Promise(resolve => setTimeout(resolve, 400))
}
}
clearAuthData()
return
}
// Restaurar sessão válida
const userData = JSON.parse(storedUser) as UserData
setToken(storedToken)
setUser(userData)
setAuthStatus('authenticated')
console.log('[AUTH] Sessão RESTAURADA com sucesso!', {
userId: userData.id,
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString()
})
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) {
console.error('[AUTH] Erro na verificação:', error)
clearAuthData()
}
}, [clearAuthData])
// Login memoizado
const login = useCallback(async (
email: string,
password: string,
userType: UserType
): Promise<boolean> => {
try {
console.log('[AUTH] Iniciando login:', { email, userType })
const response = await loginUser(email, password, userType)
saveAuthData(
response.access_token,
response.user,
response.refresh_token
)
console.log('[AUTH] Login realizado com sucesso')
return true
} catch (error) {
console.error('[AUTH] Erro no login:', error)
if (error instanceof AuthenticationError) {
throw error
}
throw new AuthenticationError(
'Erro inesperado durante o login',
'UNKNOWN_ERROR',
error
)
}
}, [saveAuthData])
// Logout memoizado
const logout = useCallback(async (): Promise<void> => {
console.log('[AUTH] Iniciando logout')
const currentUserType = user?.userType ||
(typeof window !== 'undefined' ? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) : null) ||
'profissional'
try {
if (token) {
await logoutUser(token)
console.log('[AUTH] Logout realizado na API')
}
} catch (error) {
console.error('[AUTH] Erro no logout da API:', error)
}
clearAuthData()
// Redirecionamento baseado no tipo de usuário
const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || '/login'
console.log('[AUTH] Redirecionando para:', loginRoute)
if (typeof window !== 'undefined') {
window.location.href = loginRoute
}
}, [user?.userType, token, clearAuthData])
// Refresh token memoizado (usado pelo HTTP client)
const refreshToken = useCallback(async (): Promise<boolean> => {
// Esta função é principalmente para compatibilidade
// O refresh real é feito pelo HTTP client
return false
}
}, [])
const logout = () => {
// Usar o estado atual em vez do localStorage para evitar condição de corrida
const currentUserType = userType || localStorage.getItem('userType')
// Getters memorizados
const contextValue = useMemo(() => ({
authStatus,
user,
token,
login,
logout,
refreshToken
}), [authStatus, user, token, login, logout, refreshToken])
localStorage.removeItem('isAuthenticated')
localStorage.removeItem('userEmail')
localStorage.removeItem('userType')
setIsAuthenticated(false)
setUserEmail(null)
setUserType(null)
// Redirecionar para a página de login correta baseado no tipo de usuário
if (currentUserType === 'administrador') {
router.push('/login-admin')
} else {
router.push('/login')
// Inicialização única
useEffect(() => {
if (!hasInitialized.current && typeof window !== 'undefined') {
hasInitialized.current = true
checkAuth()
}
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Carregando...</p>
</div>
</div>
)
}
}, [checkAuth])
return (
<AuthContext.Provider value={{
isAuthenticated,
userEmail,
userType,
login,
logout,
checkAuth
}}>
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
)

View File

@ -82,10 +82,24 @@ export const PATHS = {
} as const;
// Função para obter o token JWT do localStorage
function getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('auth_token');
}
function headers(kind: "json" | "form" = "json"): Record<string, string> {
const h: Record<string, string> = {};
const token = process.env.NEXT_PUBLIC_API_TOKEN?.trim();
if (token) h.Authorization = `Bearer ${token}`;
// API Key da Supabase sempre necessária
h.apikey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
// Bearer Token quando usuário está logado
const jwtToken = getAuthToken();
if (jwtToken) {
h.Authorization = `Bearer ${jwtToken}`;
}
if (kind === "json") h["Content-Type"] = "application/json";
return h;
}

388
susconecta/lib/auth.ts Normal file
View 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
View 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}`;
}

View 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(),
});
}

View 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
View 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
View 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
}
}

View File

@ -2186,6 +2186,22 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "18.3.24",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
@ -2207,6 +2223,19 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/signature_pad": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@types/signature_pad/-/signature_pad-2.3.6.tgz",
"integrity": "sha512-v3j92gCQJoxomHhd+yaG4Vsf8tRS/XbzWKqDv85UsqjMGy4zhokuwKe4b6vhbgncKkh+thF+gpz6+fypTtnFqQ==",
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@ -2336,53 +2365,6 @@
"node": ">=10.16.0"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001739",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
@ -2677,40 +2659,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -2727,6 +2675,16 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.213",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz",
@ -2775,36 +2733,6 @@
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-toolkit": {
"version": "1.39.10",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
@ -2830,6 +2758,35 @@
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"license": "Apache-2.0"
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -2843,24 +2800,6 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/geist": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/geist/-/geist-1.4.2.tgz",
@ -2870,30 +2809,6 @@
"next": ">=13.2.0"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@ -2903,37 +2818,26 @@
"node": ">=6"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/immer": {
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
@ -2963,6 +2867,12 @@
"node": ">=12"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/jiti": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
@ -3272,15 +3182,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -3441,6 +3342,35 @@
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -3491,6 +3421,69 @@
"url": "https://opencollective.com/preact"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT",
"peer": true
},
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"license": "BSD-3-Clause",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"license": "MIT",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/quill/node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT"
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@ -3749,6 +3742,13 @@
"redux": "^5.0.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@ -3780,38 +3780,6 @@
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-function-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/signature_pad": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-2.3.2.tgz",

90
susconecta/types/auth.ts Normal file
View 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