mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
feat: unify real activity tracking, admin monitoring, and error UX
This commit is contained in:
+202
-143
@@ -4,214 +4,274 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr
|
||||
import detectEthereumProvider from "@metamask/detect-provider"
|
||||
import { ethers } from "ethers"
|
||||
import { toast } from "react-hot-toast"
|
||||
import api from "@/lib/api"
|
||||
import { auth } from "@/lib/firebase"
|
||||
import {
|
||||
signInWithEmailAndPassword,
|
||||
createUserWithEmailAndPassword,
|
||||
signOut,
|
||||
onAuthStateChanged,
|
||||
type User as FirebaseUser,
|
||||
} from "firebase/auth"
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
wallet_address: string
|
||||
name?: string
|
||||
bio?: string
|
||||
avatar?: string
|
||||
created_at: string
|
||||
last_login: string
|
||||
}
|
||||
import authService, { type User } from "@/lib/auth-service"
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
firebaseUser: FirebaseUser | null
|
||||
token: string | null
|
||||
isLoadingAuth: boolean
|
||||
authMethod: "metamask" | "firebase" | null
|
||||
authMethod: "metamask" | "email" | null
|
||||
walletAddress: string | null
|
||||
walletConnected: boolean
|
||||
connectWallet: () => Promise<void>
|
||||
loginWithEmail: (email: string, password: string) => Promise<void>
|
||||
signupWithEmail: (email: string, password: string) => Promise<void>
|
||||
logout: () => void
|
||||
showMetaMaskEmailModal: boolean
|
||||
setShowMetaMaskEmailModal: (show: boolean) => void
|
||||
connectWallet: () => Promise<boolean>
|
||||
loginWithEmail: (email: string, password: string) => Promise<boolean>
|
||||
signupWithEmail: (email: string, password: string, username?: string) => Promise<boolean>
|
||||
logout: () => Promise<void>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [firebaseUser, setFirebaseUser] = useState<FirebaseUser | null>(null)
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [isLoadingAuth, setIsLoadingAuth] = useState(true)
|
||||
const [authMethod, setAuthMethod] = useState<"metamask" | "firebase" | null>(null)
|
||||
const [authMethod, setAuthMethod] = useState<"metamask" | "email" | null>(null)
|
||||
const [walletAddress, setWalletAddress] = useState<string | null>(null)
|
||||
const [walletConnected, setWalletConnected] = useState(false)
|
||||
const [showMetaMaskEmailModal, setShowMetaMaskEmailModal] = useState(false)
|
||||
|
||||
// Initialize auth state
|
||||
// Initialize auth state from localStorage
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem("openlearnx_jwt_token")
|
||||
const storedUser = localStorage.getItem("openlearnx_user")
|
||||
const storedWallet = localStorage.getItem("openlearnx_wallet")
|
||||
|
||||
if (storedToken && storedUser && storedWallet) {
|
||||
const initializeAuth = async () => {
|
||||
try {
|
||||
setUser(JSON.parse(storedUser))
|
||||
setToken(storedToken)
|
||||
setWalletAddress(storedWallet)
|
||||
setWalletConnected(true)
|
||||
setAuthMethod("metamask")
|
||||
const storedToken = localStorage.getItem("openlearnx_jwt_token")
|
||||
const storedUser = localStorage.getItem("openlearnx_user")
|
||||
const storedMethod = localStorage.getItem("openlearnx_auth_method") as "metamask" | "email" | null
|
||||
|
||||
if (storedToken) {
|
||||
// Verify token is still valid
|
||||
const verification = await authService.verifyToken(storedToken)
|
||||
|
||||
if (verification.valid && verification.user) {
|
||||
setToken(storedToken)
|
||||
setUser(verification.user)
|
||||
setAuthMethod(storedMethod || "email")
|
||||
|
||||
// If MetaMask, restore wallet address
|
||||
if (storedMethod === "metamask") {
|
||||
const storedWallet = localStorage.getItem("openlearnx_wallet")
|
||||
if (storedWallet) {
|
||||
setWalletAddress(storedWallet)
|
||||
setWalletConnected(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Token expired or invalid
|
||||
authService.clearToken()
|
||||
setToken(null)
|
||||
setUser(null)
|
||||
setAuthMethod(null)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
localStorage.clear()
|
||||
console.error("Auth initialization error:", error)
|
||||
authService.clearToken()
|
||||
} finally {
|
||||
setIsLoadingAuth(false)
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
|
||||
if (currentUser && authMethod !== "metamask") {
|
||||
setFirebaseUser(currentUser)
|
||||
setAuthMethod("firebase")
|
||||
} else if (!currentUser && authMethod === "firebase") {
|
||||
setFirebaseUser(null)
|
||||
setAuthMethod(null)
|
||||
}
|
||||
setIsLoadingAuth(false)
|
||||
})
|
||||
initializeAuth()
|
||||
}, [])
|
||||
|
||||
return () => unsubscribe()
|
||||
}, [authMethod])
|
||||
|
||||
const connectWallet = useCallback(async () => {
|
||||
/**
|
||||
* Connect MetaMask wallet
|
||||
*/
|
||||
const connectWallet = useCallback(async (): Promise<boolean> => {
|
||||
setIsLoadingAuth(true)
|
||||
|
||||
|
||||
try {
|
||||
const provider = await detectEthereumProvider()
|
||||
if (!provider) {
|
||||
toast.error("MetaMask not detected. Please install it.")
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// Create ethers provider from MetaMask
|
||||
const ethProvider = new ethers.BrowserProvider(provider as any)
|
||||
|
||||
// Request accounts
|
||||
const accounts = await ethProvider.send("eth_requestAccounts", [])
|
||||
if (accounts.length === 0) {
|
||||
toast.error("No accounts connected.")
|
||||
return
|
||||
toast.error("No MetaMask accounts found.")
|
||||
return false
|
||||
}
|
||||
|
||||
const walletAddr = accounts[0]
|
||||
const walletAddr = accounts[0].toLowerCase()
|
||||
|
||||
// Get nonce from backend
|
||||
const nonceResponse = await api.post("/api/auth/nonce", {
|
||||
wallet_address: walletAddr,
|
||||
})
|
||||
|
||||
if (!nonceResponse.data.success) {
|
||||
throw new Error(nonceResponse.data.error || "Failed to get nonce")
|
||||
const nonceResponse = await authService.getNonce(walletAddr)
|
||||
if (!nonceResponse.success || !nonceResponse.message) {
|
||||
toast.error(nonceResponse.error || "Failed to get authentication nonce")
|
||||
return false
|
||||
}
|
||||
|
||||
const { message } = nonceResponse.data
|
||||
|
||||
// Sign message
|
||||
// Sign message with MetaMask
|
||||
const signer = await ethProvider.getSigner()
|
||||
const signature = await signer.signMessage(message)
|
||||
|
||||
// Verify signature
|
||||
const verifyResponse = await api.post("/api/auth/verify", {
|
||||
wallet_address: walletAddr,
|
||||
signature,
|
||||
message,
|
||||
})
|
||||
|
||||
if (verifyResponse.data.success) {
|
||||
const { token, user } = verifyResponse.data
|
||||
|
||||
// Update states
|
||||
setToken(token)
|
||||
setUser(user)
|
||||
setWalletAddress(walletAddr)
|
||||
setWalletConnected(true)
|
||||
setFirebaseUser(null)
|
||||
setAuthMethod("metamask")
|
||||
|
||||
// Store in localStorage
|
||||
localStorage.setItem("openlearnx_jwt_token", token)
|
||||
localStorage.setItem("openlearnx_user", JSON.stringify(user))
|
||||
localStorage.setItem("openlearnx_wallet", walletAddr)
|
||||
|
||||
toast.success(`Welcome! 🦊`)
|
||||
|
||||
// ✅ CRITICAL: Redirect to dashboard after successful login
|
||||
setTimeout(() => {
|
||||
window.location.href = "/dashboard"
|
||||
}, 1000)
|
||||
|
||||
} else {
|
||||
throw new Error("Authentication failed")
|
||||
let signature: string
|
||||
|
||||
try {
|
||||
signature = await signer.signMessage(nonceResponse.message)
|
||||
} catch (signError: any) {
|
||||
if (signError.message?.includes("user rejected")) {
|
||||
toast.error("You rejected the signature request")
|
||||
} else {
|
||||
toast.error("Failed to sign message")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify signature with backend
|
||||
const verifyResponse = await authService.verifySignature(walletAddr, signature, nonceResponse.message)
|
||||
|
||||
if (!verifyResponse.success || !verifyResponse.token || !verifyResponse.user) {
|
||||
toast.error(verifyResponse.error || "Authentication failed")
|
||||
return false
|
||||
}
|
||||
|
||||
// Update state
|
||||
const { token: newToken, user: newUser } = verifyResponse
|
||||
setToken(newToken)
|
||||
setUser(newUser)
|
||||
setWalletAddress(walletAddr)
|
||||
setWalletConnected(true)
|
||||
setAuthMethod("metamask")
|
||||
|
||||
// Store in localStorage
|
||||
authService.setToken(newToken)
|
||||
localStorage.setItem("openlearnx_user", JSON.stringify(newUser))
|
||||
localStorage.setItem("openlearnx_wallet", walletAddr)
|
||||
localStorage.setItem("openlearnx_auth_method", "metamask")
|
||||
|
||||
toast.success("Connected to MetaMask! Now add your contact email")
|
||||
|
||||
// Show email modal for contact information
|
||||
setShowMetaMaskEmailModal(true)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error("MetaMask error:", error)
|
||||
toast.error(error.message || "Failed to connect MetaMask")
|
||||
console.error("MetaMask connection error:", error)
|
||||
toast.error("Failed to connect MetaMask")
|
||||
return false
|
||||
} finally {
|
||||
setIsLoadingAuth(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const loginWithEmail = useCallback(async (email: string, password: string) => {
|
||||
/**
|
||||
* Login with email and password
|
||||
*/
|
||||
const loginWithEmail = useCallback(async (email: string, password: string): Promise<boolean> => {
|
||||
setIsLoadingAuth(true)
|
||||
|
||||
try {
|
||||
await signInWithEmailAndPassword(auth, email, password)
|
||||
const response = await authService.login(email, password)
|
||||
|
||||
if (!response.success || !response.token || !response.user) {
|
||||
toast.error(response.error || "Login failed")
|
||||
return false
|
||||
}
|
||||
|
||||
// Update state
|
||||
const { token: newToken, user: newUser } = response
|
||||
setToken(newToken)
|
||||
setUser(newUser)
|
||||
setAuthMethod("email")
|
||||
setWalletConnected(false)
|
||||
setWalletAddress(null)
|
||||
|
||||
// Store in localStorage
|
||||
authService.setToken(newToken)
|
||||
localStorage.setItem("openlearnx_user", JSON.stringify(newUser))
|
||||
localStorage.setItem("openlearnx_auth_method", "email")
|
||||
|
||||
toast.success("Logged in successfully")
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error("Email login error:", error)
|
||||
toast.error("Login failed. Please try again.")
|
||||
return false
|
||||
} finally {
|
||||
setIsLoadingAuth(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Signup with email and password
|
||||
*/
|
||||
const signupWithEmail = useCallback(
|
||||
async (email: string, password: string, username?: string): Promise<boolean> => {
|
||||
setIsLoadingAuth(true)
|
||||
|
||||
try {
|
||||
const response = await authService.signup(email, password, username)
|
||||
|
||||
if (!response.success || !response.token || !response.user) {
|
||||
toast.error(response.error || "Signup failed")
|
||||
return false
|
||||
}
|
||||
|
||||
// Update state
|
||||
const { token: newToken, user: newUser } = response
|
||||
setToken(newToken)
|
||||
setUser(newUser)
|
||||
setAuthMethod("email")
|
||||
setWalletConnected(false)
|
||||
setWalletAddress(null)
|
||||
|
||||
// Store in localStorage
|
||||
authService.setToken(newToken)
|
||||
localStorage.setItem("openlearnx_user", JSON.stringify(newUser))
|
||||
localStorage.setItem("openlearnx_auth_method", "email")
|
||||
|
||||
toast.success("Account created successfully")
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error("Email signup error:", error)
|
||||
toast.error("Signup failed. Please try again.")
|
||||
return false
|
||||
} finally {
|
||||
setIsLoadingAuth(false)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*/
|
||||
const logout = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
await authService.logout()
|
||||
|
||||
// Clear state
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
setAuthMethod(null)
|
||||
setWalletAddress(null)
|
||||
setWalletConnected(false)
|
||||
toast.success("Logged in with email!")
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Email login failed")
|
||||
throw error
|
||||
} finally {
|
||||
setIsLoadingAuth(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const signupWithEmail = useCallback(async (email: string, password: string) => {
|
||||
setIsLoadingAuth(true)
|
||||
try {
|
||||
await createUserWithEmailAndPassword(auth, email, password)
|
||||
toast.success("Account created!")
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Signup failed")
|
||||
throw error
|
||||
} finally {
|
||||
setIsLoadingAuth(false)
|
||||
}
|
||||
}, [])
|
||||
// Clear storage
|
||||
authService.clearToken()
|
||||
localStorage.removeItem("openlearnx_auth_method")
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
setUser(null)
|
||||
setFirebaseUser(null)
|
||||
setToken(null)
|
||||
setWalletAddress(null)
|
||||
setWalletConnected(false)
|
||||
setAuthMethod(null)
|
||||
localStorage.clear()
|
||||
|
||||
try {
|
||||
await signOut(auth)
|
||||
toast.success("Logged out successfully!")
|
||||
} catch (error) {
|
||||
console.error("Logout error:", error)
|
||||
toast.error("Logout failed")
|
||||
}
|
||||
|
||||
toast.success("Logged out!")
|
||||
}, [])
|
||||
|
||||
const value = {
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
firebaseUser,
|
||||
token,
|
||||
isLoadingAuth,
|
||||
authMethod,
|
||||
walletAddress,
|
||||
walletConnected,
|
||||
showMetaMaskEmailModal,
|
||||
setShowMetaMaskEmailModal,
|
||||
connectWallet,
|
||||
loginWithEmail,
|
||||
signupWithEmail,
|
||||
@@ -221,13 +281,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) {
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// ✅ CRITICAL: Default export to fix the "invalid element type" error
|
||||
export default AuthProvider
|
||||
|
||||
Reference in New Issue
Block a user