ai-wpa/contexts/AuthContext.tsx

338 lines
8.7 KiB
TypeScript

'use client'
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'
export interface User {
id: string
email: string
name: string
role: 'user' | 'admin'
avatar?: string
isVerified: boolean
provider: string
createdAt: string
updatedAt: string
siliconId?: string
balance?: number
}
interface AuthContextType {
user: User | null
balance: number | null
loading: boolean
error: string | null
login: (emailOrId: string, password: string, rememberMe?: boolean) => Promise<void>
register: (email: string, name: string, password: string) => Promise<void>
logout: () => Promise<void>
refreshUser: () => Promise<void>
checkAuth: () => Promise<void>
getBalance: () => Promise<number>
addBalance: (
amount: number,
currency: 'INR' | 'USD'
) => Promise<{ success: boolean; paymentUrl?: string; formData?: any; error?: string }>
refreshBalance: () => Promise<void>
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
interface AuthProviderProps {
children: ReactNode
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null)
const [balance, setBalance] = useState<number | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [hasCheckedAuth, setHasCheckedAuth] = useState(false)
// API call helper
const apiCall = async (endpoint: string, options: RequestInit = {}) => {
const response = await fetch(`/api/auth${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
credentials: 'include', // Include cookies
...options,
})
const data = await response.json()
if (!data.success) {
throw new Error(data.error?.message || 'An error occurred')
}
return data
}
// Balance API call helper
const balanceApiCall = async (endpoint: string, options: RequestInit = {}) => {
const response = await fetch(`/api${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
credentials: 'include',
...options,
})
const data = await response.json()
return data
}
// Check if user is authenticated - only when explicitly needed
const checkAuth = async () => {
// Skip if already checked in this session or if user is already set
if (hasCheckedAuth || user) {
setLoading(false)
return
}
try {
setLoading(true)
setError(null)
const data = await apiCall('/me')
setUser(data.data.user)
setBalance(data.data.user.balance || null)
setHasCheckedAuth(true)
} catch (err) {
// If getting user info fails, try to refresh token
try {
const refreshData = await apiCall('/refresh', { method: 'POST' })
setUser(refreshData.data.user)
setBalance(refreshData.data.user.balance || null)
setHasCheckedAuth(true)
} catch (refreshErr) {
// Both failed, user is not authenticated
setUser(null)
setBalance(null)
setHasCheckedAuth(true)
}
} finally {
setLoading(false)
}
}
// Login function
const login = async (emailOrId: string, password: string, rememberMe?: boolean) => {
try {
setLoading(true)
setError(null)
const data = await apiCall('/login', {
method: 'POST',
body: JSON.stringify({ emailOrId, password, rememberMe }),
})
setUser(data.data.user)
setBalance(data.data.user.balance || null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed')
throw err
} finally {
setLoading(false)
}
}
// Register function
const register = async (email: string, name: string, password: string) => {
try {
setLoading(true)
setError(null)
const data = await apiCall('/register', {
method: 'POST',
body: JSON.stringify({ email, name, password }),
})
setUser(data.data.user)
setBalance(data.data.user.balance || null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Registration failed')
throw err
} finally {
setLoading(false)
}
}
// Logout function
const logout = async () => {
try {
setLoading(true)
await apiCall('/logout', { method: 'POST' })
setUser(null)
setBalance(null)
} catch (err) {
console.error('Logout error:', err)
// Even if logout API fails, clear user state
setUser(null)
setBalance(null)
} finally {
setLoading(false)
}
}
// Refresh user data
const refreshUser = async () => {
try {
const data = await apiCall('/me')
setUser(data.data.user)
setBalance(data.data.user.balance || null)
} catch (err) {
console.error('Refresh user error:', err)
setUser(null)
setBalance(null)
}
}
// Get user balance
const getBalance = async (): Promise<number> => {
try {
const data = await balanceApiCall('/user/balance')
if (data.success) {
return data.data.balance
}
throw new Error(data.error?.message || 'Failed to fetch balance')
} catch (err) {
console.error('Get balance error:', err)
throw err
}
}
// Add balance (initiate payment)
const addBalance = async (amount: number, currency: 'INR' | 'USD' = 'INR') => {
try {
const data = await balanceApiCall('/balance/add', {
method: 'POST',
body: JSON.stringify({ amount, currency }),
})
if (data.success) {
return {
success: true,
paymentUrl: data.payment_url,
formData: data.form_data,
}
} else {
return {
success: false,
error: data.message || 'Failed to initiate payment',
}
}
} catch (err) {
console.error('Add balance error:', err)
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to add balance',
}
}
}
// Refresh balance and update user state
const refreshBalance = async () => {
try {
const newBalance = await getBalance()
setBalance(newBalance)
if (user) {
setUser({ ...user, balance: newBalance })
}
} catch (err) {
console.error('Refresh balance error:', err)
}
}
// Check for existing session on mount (restore user state)
useEffect(() => {
const restoreSession = async () => {
try {
setLoading(true)
const data = await apiCall('/me')
setUser(data.data.user)
setBalance(data.data.user.balance || null)
setHasCheckedAuth(true)
} catch (err) {
// If /me fails, try refresh token
try {
const refreshData = await apiCall('/refresh', { method: 'POST' })
setUser(refreshData.data.user)
setBalance(refreshData.data.user.balance || null)
setHasCheckedAuth(true)
} catch (refreshErr) {
// No valid session
setUser(null)
setBalance(null)
setHasCheckedAuth(true)
}
} finally {
setLoading(false)
}
}
restoreSession()
}, [])
// Set up auto-refresh only when user is logged in
useEffect(() => {
if (!user) return
// Auto-refresh user data when tab becomes visible (after 5+ minutes)
let lastRefresh = Date.now()
const handleVisibilityChange = () => {
if (!document.hidden && Date.now() - lastRefresh > 5 * 60 * 1000) {
refreshUser()
lastRefresh = Date.now()
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => document.removeEventListener('visibilitychange', handleVisibilityChange)
}, [user])
// Auto-refresh token every 10 minutes if user is logged in
useEffect(() => {
if (!user) return
const interval = setInterval(
async () => {
try {
await apiCall('/refresh', { method: 'POST' })
} catch (err) {
console.error('Auto-refresh failed:', err)
setUser(null) // Log out if refresh fails
setBalance(null)
}
},
10 * 60 * 1000
) // 10 minutes
return () => clearInterval(interval)
}, [user])
const value: AuthContextType = {
user,
balance,
loading,
error,
login,
register,
logout,
refreshUser,
checkAuth,
getBalance,
addBalance,
refreshBalance,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}