initial commit
This commit is contained in:
337
contexts/AuthContext.tsx
Normal file
337
contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
'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
|
||||
}
|
||||
35
contexts/QueryProvider.tsx
Normal file
35
contexts/QueryProvider.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactNode, useState } from 'react'
|
||||
|
||||
interface QueryProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function QueryProvider({ children }: QueryProviderProps) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// With SSR, we usually want to set some default staleTime
|
||||
// above 0 to avoid refetching immediately on the client
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
retry: (failureCount, error: any) => {
|
||||
// Don't retry on 4xx errors
|
||||
if (error?.status >= 400 && error?.status < 500) {
|
||||
return false
|
||||
}
|
||||
// Retry up to 3 times for other errors
|
||||
return failureCount < 3
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
retry: false, // Don't retry mutations by default
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
}
|
||||
Reference in New Issue
Block a user