213 lines
6.1 KiB
TypeScript
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
|
|
}
|