- What was done: - Added a server-side Next.js route at `src/app/api/create-user/route.ts` that validates the requester token, checks roles, generates a temporary password and forwards the creation to the Supabase Edge Function using the service role key. - Client wired to call the route via `lib/config.ts` (`FUNCTIONS_ENDPOINTS.CREATE_USER` -> `/api/create-user`) and the `criarUsuario()` wrapper in `lib/api.ts`. - Status / missing work: - Important: user creation is NOT working yet (requests to `/api/create-user` return 404 in dev). - Next steps: restart dev server, ensure `SUPABASE_SERVICE_ROLE_KEY` is set in the environment, check server logs and run a test POST with a valid admin JWT.
290 lines
9.8 KiB
TypeScript
290 lines
9.8 KiB
TypeScript
'use client'
|
|
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth'
|
|
import { getUserInfo } from '@/lib/api'
|
|
import { ENV_CONFIG } from '@/lib/env-config'
|
|
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 [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)
|
|
|
|
// Utilitários de armazenamento memorizados
|
|
const clearAuthData = useCallback(() => {
|
|
if (typeof window !== 'undefined') {
|
|
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
|
|
}
|
|
setUser(null)
|
|
setToken(null)
|
|
setAuthStatus('unauthenticated')
|
|
console.log('[AUTH] Dados de autenticação limpos - logout realizado')
|
|
}, [])
|
|
|
|
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)
|
|
// Garantir que roles também sejam persistidos se presentes
|
|
const toStore = { ...userData } as any
|
|
if (userData && (userData as any).roles) {
|
|
toStore.roles = (userData as any).roles
|
|
}
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(toStore))
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, toStore.userType)
|
|
|
|
if (refreshToken) {
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
|
|
}
|
|
}
|
|
|
|
setToken(accessToken)
|
|
// Garantir que o estado mantenha roles também
|
|
setUser(userData as any)
|
|
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)) {
|
|
// Refresh automático foi desativado no cliente. A renovação de tokens
|
|
// deve ser feita via backend seguro. Se desejar habilitar refresh no
|
|
// cliente, implemente um flow server-side ou use o SDK autenticado.
|
|
console.log('[AUTH] Refresh token presente, mas refresh automático desativado no cliente')
|
|
}
|
|
|
|
clearAuthData()
|
|
return
|
|
}
|
|
|
|
// Restaurar sessão válida
|
|
const userData = JSON.parse(storedUser) as UserData
|
|
setToken(storedToken)
|
|
// Nota: chamadas a user-info foram removidas do cliente. Mantemos os dados do
|
|
// usuário que já estavam persistidos localmente sem tentar reconciliar com funções remotas.
|
|
|
|
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)
|
|
|
|
// Nota: busca de user-info e reconciliação via funções server-side foi removida do cliente.
|
|
// Mantemos o objeto response.user retornado pelo fluxo de autenticação sem tentativas
|
|
// adicionais de mesclagem aqui.
|
|
|
|
// Salva dados iniciais (token + user retornado pelo login)
|
|
saveAuthData(
|
|
response.access_token,
|
|
response.user,
|
|
response.refresh_token
|
|
)
|
|
|
|
// Tentar buscar informações consolidadas do usuário (profile + roles)
|
|
try {
|
|
const userInfo = await getUserInfo()
|
|
if (userInfo) {
|
|
const mergedUser: any = {
|
|
...response.user,
|
|
// prefer profile and roles vindos do userInfo
|
|
profile: userInfo.profile ?? (response.user as any).profile,
|
|
roles: userInfo.roles ?? (response.user as any).roles ?? [],
|
|
// garantir id/email caso estejam no userInfo
|
|
id: (response.user as any).id || (userInfo.user as any)?.id,
|
|
email: (response.user as any).email || (userInfo.user as any)?.email,
|
|
}
|
|
|
|
// Inferir userType a partir de roles se estiver ausente
|
|
if (!mergedUser.userType || mergedUser.userType === undefined) {
|
|
const r: string[] = mergedUser.roles || [];
|
|
if (r.includes('admin') || r.includes('gestor') || r.includes('secretaria')) mergedUser.userType = 'administrador'
|
|
else if (r.includes('medico') || r.includes('enfermeiro')) mergedUser.userType = 'profissional'
|
|
else if (r.includes('paciente')) mergedUser.userType = 'paciente'
|
|
}
|
|
|
|
// Persistir e atualizar estado com dados consolidados
|
|
try {
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(mergedUser))
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, mergedUser.userType || '')
|
|
}
|
|
setUser(mergedUser as any)
|
|
console.log('[AUTH] userInfo consolidado e salvo:', { email: mergedUser.email, roles: mergedUser.roles })
|
|
} catch (e) {
|
|
console.warn('[AUTH] Não foi possível persistir userInfo consolidado:', e)
|
|
}
|
|
}
|
|
} catch (infoErr) {
|
|
console.warn('[AUTH] getUserInfo falhou (ignorar no cliente):', infoErr)
|
|
}
|
|
|
|
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
|
|
}, [])
|
|
|
|
// Getters memorizados
|
|
const contextValue = useMemo(() => ({
|
|
authStatus,
|
|
user,
|
|
token,
|
|
login,
|
|
logout,
|
|
refreshToken
|
|
}), [authStatus, user, token, login, logout, refreshToken])
|
|
|
|
// Inicialização única
|
|
useEffect(() => {
|
|
if (!hasInitialized.current && typeof window !== 'undefined') {
|
|
hasInitialized.current = true
|
|
checkAuth()
|
|
}
|
|
}, [checkAuth])
|
|
|
|
return (
|
|
<AuthContext.Provider value={contextValue}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
)
|
|
}
|
|
|
|
export const useAuth = () => {
|
|
const context = useContext(AuthContext)
|
|
if (context === undefined) {
|
|
throw new Error('useAuth deve ser usado dentro de AuthProvider')
|
|
}
|
|
return context
|
|
} |