beetask/frontend/src/contexts/AuthContext.tsx

213 lines
6.1 KiB
TypeScript

import {
createContext,
useContext,
useState,
useEffect,
useCallback,
useMemo,
type ReactNode,
} from 'react'
import {
authService,
type LoginResponse,
type CreateAccountResponse,
type OrganizationMembership,
} from '../services/authService'
interface AuthContextType {
user: LoginResponse | null
isAuthenticated: boolean
isLoading: boolean
activeOrg: OrganizationMembership | null
login: (email: string, password: string) => Promise<LoginResponse>
register: (name: string, email: string, password: string) => Promise<CreateAccountResponse>
logout: () => void
refreshUser: () => Promise<void>
updateUser: (updated: Partial<LoginResponse>) => void
setActiveOrg: (id: number) => void
}
const STORAGE_KEYS = {
USER: 'user',
ACTIVE_ORG_ID: 'activeOrgId',
} as const
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<LoginResponse | null>(null)
const [activeOrgId, setActiveOrgId] = useState<number | null>(null)
const [isLoading, setIsLoading] = useState(true)
const setupActiveOrg = useCallback((organizations: OrganizationMembership[]) => {
const storedOrgId = localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID)
const parsedStoredId = storedOrgId ? Number(storedOrgId) : null
const isValidOrg = organizations.some((org) => org.id === parsedStoredId)
if (isValidOrg && parsedStoredId !== null) {
setActiveOrgId(parsedStoredId)
} else if (organizations.length > 0) {
const firstOrgId = organizations[0].id
setActiveOrgId(firstOrgId)
localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, firstOrgId.toString())
}
}, [])
const logout = useCallback(() => {
localStorage.removeItem(STORAGE_KEYS.USER)
localStorage.removeItem(STORAGE_KEYS.ACTIVE_ORG_ID)
setUser(null)
setActiveOrgId(null)
}, [])
const renewToken = useCallback(async (refreshToken: string) => {
try {
const response = await authService.refresh(refreshToken)
response.issuedAt = new Date().toISOString()
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(response))
setUser(response)
return response
} catch {
logout()
return null
}
}, [logout])
useEffect(() => {
const initAuth = async () => {
const storedUser = localStorage.getItem(STORAGE_KEYS.USER)
if (storedUser) {
try {
const userData: LoginResponse = JSON.parse(storedUser)
const issuedAt = new Date(userData.issuedAt).getTime()
const expiresAt = issuedAt + userData.expiresIn * 1000
const now = Date.now()
if (now >= expiresAt) {
if (userData.refreshToken) {
const renewed = await renewToken(userData.refreshToken)
if (renewed) {
setupActiveOrg(renewed.organizations)
}
} else {
logout()
}
} else {
setUser(userData)
setupActiveOrg(userData.organizations)
// Se expirar em menos de 24 horas, renova proativamente
if (expiresAt - now < 86400000 && userData.refreshToken) {
renewToken(userData.refreshToken)
}
}
} catch {
localStorage.removeItem(STORAGE_KEYS.USER)
}
}
setIsLoading(false)
}
initAuth()
}, [logout, renewToken, setupActiveOrg])
// Verificação periódica a cada 60 segundos
useEffect(() => {
if (!user) return
const interval = setInterval(() => {
const issuedAt = new Date(user.issuedAt).getTime()
const expiresAt = issuedAt + user.expiresIn * 1000
const now = Date.now()
if (now >= expiresAt) {
if (user.refreshToken) {
renewToken(user.refreshToken)
} else {
logout()
}
} else if (expiresAt - now < 86400000 && user.refreshToken) {
// Se expirar em menos de 24 horas, renova proativamente
renewToken(user.refreshToken)
}
}, 60000)
return () => clearInterval(interval)
}, [user, logout, renewToken])
const login = async (email: string, password: string) => {
const response = await authService.login(email, password)
response.issuedAt = new Date().toISOString()
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(response))
setUser(response)
setupActiveOrg(response.organizations)
return response
}
const register = async (name: string, email: string, password: string) => {
return authService.register(name, email, password)
}
const refreshUser = async () => {
try {
const response = await authService.me()
const storedUser = localStorage.getItem(STORAGE_KEYS.USER)
if (storedUser) {
const userData = JSON.parse(storedUser)
const updatedUser = { ...userData, ...response }
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(updatedUser))
setUser(updatedUser)
setupActiveOrg(updatedUser.organizations)
}
} catch {
// Ignora erro
}
}
const updateUser = useCallback((updated: Partial<LoginResponse>) => {
setUser((prev) => {
if (!prev) return null
const next = { ...prev, ...updated }
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(next))
return next
})
}, [])
const setActiveOrg = (id: number) => {
setActiveOrgId(id)
localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, id.toString())
}
const activeOrg = useMemo(
() => user?.organizations.find((org) => org.id === activeOrgId) || null,
[user, activeOrgId]
)
return (
<AuthContext.Provider
value={{
user,
isAuthenticated: !!user,
isLoading,
activeOrg,
login,
register,
logout,
refreshUser,
updateUser,
setActiveOrg,
}}
>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}