338 lines
8.7 KiB
TypeScript
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
|
|
}
|