diff --git a/backend/main.py b/backend/main.py
index 8333445..18306c1 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -13,6 +13,9 @@ from pymongo import MongoClient
from bson import ObjectId
import hashlib
import time
+import signal
+import io
+from contextlib import redirect_stdout, redirect_stderr
# Load environment variables
load_dotenv()
@@ -276,7 +279,7 @@ def get_comprehensive_stats():
# Calculate real-time statistics
current_time = datetime.now()
join_date = user_stats.get('join_date', current_time - timedelta(days=30)) if user_stats else current_time - timedelta(days=30)
- days_since_join = (current_time - join_date).days if days_since_join > 0 else 30
+ days_since_join = (current_time - join_date).days if (current_time - join_date).days > 0 else 30
# ✅ ENHANCED: Calculate coding streak with proper logic
coding_streak = calculate_coding_streak(db, user_id)
@@ -381,34 +384,41 @@ def get_recent_activity():
]
for collection, activity_type, default_title, default_points in activity_sources:
- recent_items = collection.find(
- {"user_id": user_id}
- ).sort([("completed_at", -1), ("submitted_at", -1), ("earned_at", -1), ("issued_at", -1)]).limit(max_records // len(activity_sources))
-
- for item in recent_items:
- # Determine the completion date field
- completed_at = (
- item.get('completed_at') or
- item.get('submitted_at') or
- item.get('earned_at') or
- item.get('issued_at') or
- datetime.now()
- )
+ try:
+ recent_items = collection.find(
+ {"user_id": user_id}
+ ).sort([("completed_at", -1), ("submitted_at", -1), ("earned_at", -1), ("issued_at", -1)]).limit(max_records // len(activity_sources))
- if isinstance(completed_at, str):
- completed_at = datetime.fromisoformat(completed_at)
-
- activities.append({
- "id": str(item.get('_id', uuid.uuid4())),
- "type": activity_type,
- "title": item.get('title', item.get('name', default_title)),
- "description": format_activity_description(item, activity_type),
- "completed_at": completed_at.isoformat(),
- "points_earned": item.get('points', item.get('points_earned', default_points)),
- "success_rate": item.get('score', item.get('completion_percentage', 100)),
- "difficulty": item.get('difficulty', 'Intermediate'),
- "blockchain_verified": item.get('blockchain_verified', False)
- })
+ for item in recent_items:
+ # Determine the completion date field
+ completed_at = (
+ item.get('completed_at') or
+ item.get('submitted_at') or
+ item.get('earned_at') or
+ item.get('issued_at') or
+ datetime.now()
+ )
+
+ if isinstance(completed_at, str):
+ try:
+ completed_at = datetime.fromisoformat(completed_at)
+ except:
+ completed_at = datetime.now()
+
+ activities.append({
+ "id": str(item.get('_id', uuid.uuid4())),
+ "type": activity_type,
+ "title": item.get('title', item.get('name', default_title)),
+ "description": format_activity_description(item, activity_type),
+ "completed_at": completed_at.isoformat(),
+ "points_earned": item.get('points', item.get('points_earned', default_points)),
+ "success_rate": item.get('score', item.get('completion_percentage', 100)),
+ "difficulty": item.get('difficulty', 'Intermediate'),
+ "blockchain_verified": item.get('blockchain_verified', False)
+ })
+ except Exception as e:
+ logger.warning(f"⚠️ Failed to fetch {activity_type} activities: {e}")
+ continue
# Sort all activities by completion date
activities.sort(key=lambda x: x['completed_at'], reverse=True)
@@ -559,7 +569,10 @@ def calculate_coding_streak(db, user_id):
for submission in submissions:
submission_date = submission.get('submitted_at')
if isinstance(submission_date, str):
- submission_date = datetime.fromisoformat(submission_date).date()
+ try:
+ submission_date = datetime.fromisoformat(submission_date).date()
+ except:
+ continue
elif isinstance(submission_date, datetime):
submission_date = submission_date.date()
else:
@@ -860,15 +873,11 @@ def format_activity_description(item, activity_type):
return "Activity completed"
# ===================================================================
-# ✅ ENHANCED DYNAMIC SCORING SYSTEM - WITH YOUR UPDATES
+# ✅ ENHANCED DYNAMIC SCORING SYSTEM
# ===================================================================
def calculate_dynamic_score(code, language, problem):
"""Enhanced dynamic scoring with better error handling and feedback"""
- import io
- from contextlib import redirect_stdout, redirect_stderr
- import time
- import signal
# Handle both old and new problem formats
test_cases = problem.get('test_cases', [])
@@ -906,11 +915,24 @@ def calculate_dynamic_score(code, language, problem):
# ✅ ENHANCED: Safer execution environment
exec_globals = {
"__builtins__": {
- **__builtins__,
- '__import__': None, # Disable imports for security
- 'open': None, # Disable file operations
- 'eval': None, # Disable eval
- 'exec': None, # Disable nested exec
+ 'print': print,
+ 'len': len,
+ 'str': str,
+ 'int': int,
+ 'float': float,
+ 'list': list,
+ 'dict': dict,
+ 'tuple': tuple,
+ 'set': set,
+ 'range': range,
+ 'enumerate': enumerate,
+ 'zip': zip,
+ 'sum': sum,
+ 'max': max,
+ 'min': min,
+ 'sorted': sorted,
+ 'abs': abs,
+ 'round': round,
},
"__name__": "__main__"
}
@@ -923,18 +945,25 @@ def calculate_dynamic_score(code, language, problem):
else:
exec_globals['input'] = lambda prompt='': ''
- # ✅ ADDED: Timeout protection
- def timeout_handler(signum, frame):
- raise TimeoutError("Code execution timed out")
-
- signal.signal(signal.SIGALRM, timeout_handler)
- signal.alarm(5) # 5 second timeout
+ # ✅ ADDED: Timeout protection (Unix-like systems only)
+ try:
+ def timeout_handler(signum, frame):
+ raise TimeoutError("Code execution timed out")
+
+ signal.signal(signal.SIGALRM, timeout_handler)
+ signal.alarm(5) # 5 second timeout
+ except:
+ # Skip timeout on Windows
+ pass
try:
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
exec(code, exec_globals)
finally:
- signal.alarm(0) # Cancel timeout
+ try:
+ signal.alarm(0) # Cancel timeout
+ except:
+ pass
actual_output = stdout_buffer.getvalue().strip()
stderr_content = stderr_buffer.getvalue().strip()
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
index a59be19..bd7d18d 100644
--- a/backend/routes/auth.py
+++ b/backend/routes/auth.py
@@ -157,4 +157,4 @@ def verify_signature():
return jsonify({
"success": False,
"error": str(e)
- }), 500
+ }), 500
\ No newline at end of file
diff --git a/backend/routes/dashboard.py b/backend/routes/dashboard.py
index 79fe2dd..28456de 100644
--- a/backend/routes/dashboard.py
+++ b/backend/routes/dashboard.py
@@ -824,4 +824,4 @@ def dashboard_root():
"/api/dashboard/update-profile"
],
"authentication": "JWT Token in Authorization header OR Wallet address in X-Wallet-Address header"
- })
+ })
\ No newline at end of file
diff --git a/frontend/app/auth/login/page.tsx b/frontend/app/auth/login/page.tsx
index 77a05b7..bf60b89 100644
--- a/frontend/app/auth/login/page.tsx
+++ b/frontend/app/auth/login/page.tsx
@@ -1,81 +1,101 @@
"use client"
-import { useState, useEffect, useRef } from "react"
+import { useEffect, useRef, useState } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/context/auth-context"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
-import { Alert, AlertDescription } from "@/components/ui/alert"
-import { Wallet, Mail, Lock, Loader2, Shield, CheckCircle2, AlertCircle } from "lucide-react"
+import { Wallet, Mail, Lock, Loader2, CheckCircle2 } from "lucide-react"
import { toast } from "react-hot-toast"
export default function LoginPage() {
const {
- connectWallet,
- loginWithEmail,
- isLoadingAuth,
+ user,
+ firebaseUser,
walletConnected,
walletAddress,
- firebaseUser,
- authMethod
+ isLoadingAuth,
+ authMethod,
+ connectWallet,
+ loginWithEmail
} = useAuth()
const router = useRouter()
+ const hasRedirected = useRef(false)
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isEmailLogin, setIsEmailLogin] = useState(false)
- const [isConnectingWallet, setIsConnectingWallet] = useState(false)
const [isSubmittingEmail, setIsSubmittingEmail] = useState(false)
- const hasRedirected = useRef(false)
- // ✅ Check for existing authentication
+ // ✅ FIXED: More comprehensive redirect logic with debug logging
useEffect(() => {
- if (hasRedirected.current || isLoadingAuth) return
+ console.log("🔍 Login page - checking auth state:", {
+ isLoadingAuth,
+ hasRedirected: hasRedirected.current,
+ user: !!user,
+ firebaseUser: !!firebaseUser,
+ walletConnected,
+ walletAddress,
+ authMethod
+ })
- const checkAuth = setTimeout(() => {
- if (isLoadingAuth) return
+ // Don't redirect if still loading or already redirected
+ if (isLoadingAuth || hasRedirected.current) {
+ console.log("⏳ Skipping redirect - loading or already redirected")
+ return
+ }
- const isAuthenticated = (walletConnected && walletAddress) || firebaseUser
+ // Check for successful authentication
+ const isMetaMaskAuth = walletConnected && walletAddress && user && authMethod === "metamask"
+ const isFirebaseAuth = firebaseUser && authMethod === "firebase"
+ const isAuthenticated = isMetaMaskAuth || isFirebaseAuth
- if (isAuthenticated && !hasRedirected.current) {
- console.log('✅ User already authenticated, redirecting to dashboard...')
- hasRedirected.current = true
+ console.log("🔍 Authentication check:", {
+ isMetaMaskAuth,
+ isFirebaseAuth,
+ isAuthenticated
+ })
+
+ if (isAuthenticated && !hasRedirected.current) {
+ console.log("✅ User authenticated - redirecting to dashboard...")
+ hasRedirected.current = true
+
+ // Add a small delay to ensure state is fully updated
+ setTimeout(() => {
router.replace("/dashboard")
- }
- }, 500)
+ }, 100)
+ }
+ }, [
+ user,
+ firebaseUser,
+ walletConnected,
+ walletAddress,
+ authMethod,
+ isLoadingAuth,
+ router
+ ]) // ✅ FIXED: Include all necessary dependencies
- return () => clearTimeout(checkAuth)
- }, [isLoadingAuth, walletConnected, walletAddress, firebaseUser, router])
-
- // ✅ Handle MetaMask connection
- const handleWalletConnect = async () => {
- if (isConnectingWallet || isLoadingAuth) return
-
- setIsConnectingWallet(true)
-
+ // ✅ Handle MetaMask connection with immediate redirect check
+ const handleMetaMaskLogin = async () => {
try {
- console.log('🦊 Starting MetaMask connection...')
+ console.log("🦊 Starting MetaMask login...")
+ await connectWallet()
+ console.log("🦊 MetaMask login completed, checking for redirect...")
- // Check if MetaMask is installed
- if (typeof window !== 'undefined' && !window.ethereum) {
- toast.error("MetaMask not detected. Please install MetaMask extension.")
- window.open('https://metamask.io/download/', '_blank')
- return
- }
-
- const success = await connectWallet()
-
- if (success) {
- console.log('✅ MetaMask connection successful')
- // Redirect will be handled by useEffect
- }
- } catch (error: any) {
- console.error('❌ Wallet connection error:', error)
- } finally {
- setIsConnectingWallet(false)
+ // Force a redirect check after a short delay
+ setTimeout(() => {
+ const isAuth = walletConnected && walletAddress && user && authMethod === "metamask"
+ if (isAuth && !hasRedirected.current) {
+ console.log("🔄 Force redirecting after MetaMask success...")
+ hasRedirected.current = true
+ router.replace("/dashboard")
+ }
+ }, 500)
+ } catch (error) {
+ console.error("❌ MetaMask login failed:", error)
}
}
@@ -83,8 +103,6 @@ export default function LoginPage() {
const handleEmailLogin = async (e: React.FormEvent) => {
e.preventDefault()
- if (isSubmittingEmail || isLoadingAuth) return
-
if (!email.trim() || !password.trim()) {
toast.error("Please enter both email and password")
return
@@ -96,34 +114,39 @@ export default function LoginPage() {
await loginWithEmail(email, password)
// Redirect will be handled by useEffect
} catch (error: any) {
- console.error('❌ Email login failed:', error)
- toast.error(error.message || "Login failed. Please check your credentials.")
+ console.error("❌ Email login failed:", error)
+ toast.error(error.message || "Login failed")
} finally {
setIsSubmittingEmail(false)
}
}
- // Show connected state
- if ((walletConnected && walletAddress) || firebaseUser) {
+ // ✅ Show success state when authenticated but not yet redirected
+ const isAuthenticated = (walletConnected && walletAddress && user) || firebaseUser
+
+ if (isAuthenticated && !hasRedirected.current) {
return (
- {walletConnected ? "MetaMask Connected! 🦊" : "Email Login Successful! 📧"}
+ Login Successful! ✅
-
-
- {walletConnected
- ? `🦊 ${walletAddress?.slice(0, 6)}...${walletAddress?.slice(-4)}`
- : `📧 ${firebaseUser?.email}`
- }
-
-
+
+ {authMethod === "metamask"
+ ? `🦊 MetaMask connected: ${walletAddress?.slice(0, 6)}...${walletAddress?.slice(-4)}`
+ : `📧 Email: ${firebaseUser?.email}`
+ }
+
+
+
+ Redirecting to dashboard...
+
+ {/* Manual redirect button as backup */}
@@ -141,26 +164,11 @@ export default function LoginPage() {
)
}
- // Show loading while initializing
- if (isLoadingAuth) {
- return (
-
- )
- }
-
+ // ✅ Show login form
return (
-
-
-
-
Welcome to OpenLearnX! 🎓
@@ -170,11 +178,11 @@ export default function LoginPage() {
{/* MetaMask Login */}
@@ -231,14 +239,14 @@ export default function LoginPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
- disabled={isSubmittingEmail || isConnectingWallet}
+ disabled={isSubmittingEmail || isLoadingAuth}
required
/>
{isSubmittingEmail ? (
@@ -256,23 +264,6 @@ export default function LoginPage() {
)}
-
- {/* MetaMask Installation Help */}
- {typeof window !== 'undefined' && !window.ethereum && (
-
-
-
- MetaMask not detected.
- window.open('https://metamask.io/download/', '_blank')}
- >
- Install MetaMask →
-
-
-
- )}
diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx
index 3289175..5007923 100644
--- a/frontend/app/dashboard/page.tsx
+++ b/frontend/app/dashboard/page.tsx
@@ -1,121 +1,374 @@
"use client"
+import { useAuth } from "@/context/auth-context"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
-import { useAuth } from "@/context/auth-context"
-import { DashboardStatsOverview } from "@/components/dashboard-stats"
-import { Loader2, AlertCircle } from "lucide-react"
-import { Button } from "@/components/ui/button"
+import {
+ User,
+ LogOut,
+ Settings,
+ Trophy,
+ BookOpen,
+ Target,
+ TrendingUp,
+ Wallet,
+ Mail,
+ Calendar,
+ Award,
+ BarChart3,
+ Activity,
+ Edit3,
+ Save,
+ X
+} from "lucide-react"
export default function DashboardPage() {
- const { isLoadingAuth, walletConnected, walletAddress, firebaseUser, authMethod } = useAuth()
+ const { user, firebaseUser, walletConnected, logout, authMethod } = useAuth()
const router = useRouter()
- const [showDashboard, setShowDashboard] = useState(false)
- const [debugInfo, setDebugInfo] = useState(null)
+ const [isEditingProfile, setIsEditingProfile] = useState(false)
+ const [profileData, setProfileData] = useState({
+ name: user?.name || '',
+ bio: user?.bio || '',
+ avatar: user?.avatar || ''
+ })
+ const [stats, setStats] = useState({
+ coursesCompleted: 12,
+ totalXP: 2450,
+ currentStreak: 7,
+ rank: 156,
+ certificatesEarned: 3,
+ hoursLearned: 45
+ })
+
useEffect(() => {
- // Debug authentication state
- const authState = {
- isLoadingAuth,
- walletConnected,
- walletAddress: !!walletAddress,
- firebaseUser: !!firebaseUser,
- authMethod,
- localStorage: {
- token: !!localStorage.getItem('openlearnx_jwt_token'),
- wallet: !!localStorage.getItem('openlearnx_wallet'),
- user: !!localStorage.getItem('openlearnx_user')
- }
+ if (!user && !firebaseUser) {
+ router.replace("/auth/login")
}
-
- setDebugInfo(authState)
- console.log('📊 Dashboard auth state:', authState)
+ }, [user, firebaseUser, router])
- // Give auth some time to initialize
- const timer = setTimeout(() => {
- const isAuthenticated = (walletConnected && walletAddress) || firebaseUser
-
- if (isAuthenticated) {
- console.log('✅ User authenticated, showing dashboard')
- setShowDashboard(true)
- } else if (!isLoadingAuth) {
- console.log('❌ User not authenticated, redirecting to login')
- router.replace("/auth/login")
- }
- }, 2000) // Wait 2 seconds for auth to stabilize
+ const handleProfileUpdate = async () => {
+ try {
+ // Here you would call your API to update profile
+ // await updateProfile(profileData)
+ setIsEditingProfile(false)
+ console.log("Profile updated:", profileData)
+ } catch (error) {
+ console.error("Failed to update profile:", error)
+ }
+ }
- return () => clearTimeout(timer)
- }, [isLoadingAuth, walletConnected, walletAddress, firebaseUser, authMethod, router])
-
- // Show loading state
- if (isLoadingAuth || !showDashboard) {
+ if (!user && !firebaseUser) {
return (
-
-
-
-
-
- Loading Dashboard...
-
-
- {walletConnected ? `Connected to ${walletAddress?.slice(0, 6)}...${walletAddress?.slice(-4)}` :
- firebaseUser ? `Logged in as ${firebaseUser.email}` :
- 'Verifying authentication...'}
-
-
-
- {/* Debug info in development */}
- {process.env.NODE_ENV === 'development' && debugInfo && (
-
- Debug Info
- {JSON.stringify(debugInfo, null, 2)}
-
- )}
-
+
)
}
- // Show error state if no auth after loading
- if (!walletConnected && !firebaseUser && !isLoadingAuth) {
- return (
-
-
-
-
- Authentication Required
-
-
- Please log in to access your dashboard.
-
-
-
router.push("/auth/login")} className="w-full">
- Go to Login
-
-
{
- localStorage.clear()
- window.location.href = "/auth/login"
- }}
- className="w-full"
- >
- Clear Data & Login
-
+ return (
+
+ {/* Professional Header */}
+
- )
- }
+
- // Show dashboard if authenticated
- return
+ {/* Main Dashboard Content */}
+
+ {/* Welcome Section */}
+
+
+
+
+
+ Welcome back! 👋
+
+
+ Ready to continue your learning journey?
+
+ {authMethod === "metamask" && user ? (
+
+
+
+ Connected: {user.wallet_address.slice(0, 6)}...{user.wallet_address.slice(-4)}
+
+
+ ) : firebaseUser && (
+
+
+
+ {firebaseUser.email}
+
+
+ )}
+
+
+
+
+
+
+ {/* Stats Grid */}
+
+
+
+
+
Total XP
+
{stats.totalXP.toLocaleString()}
+
+
+
+
+
+
+
+ +12% from last week
+
+
+
+
+
+
+
Courses
+
{stats.coursesCompleted}
+
+
+
+
+
+
+
+
+
+
+
+
Streak
+
{stats.currentStreak} days
+
+
+
+
+
+
+ 🔥 Keep it up!
+
+
+
+
+
+
+
Global Rank
+
#{stats.rank}
+
+
+
+
+
+
+
+
+
+ {/* Main Content Grid */}
+
+ {/* Profile Card with Edit Functionality */}
+
+
+
+
Profile
+ setIsEditingProfile(!isEditingProfile)}
+ className="p-2 text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition-all duration-200"
+ >
+ {isEditingProfile ? : }
+
+
+
+
+
+
+
+ {isEditingProfile ? (
+
+ setProfileData({...profileData, name: e.target.value})}
+ placeholder="Your name"
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-center"
+ />
+
+ ) : (
+
+
+ {profileData.name || "Your Name"}
+
+
+ {profileData.bio || "Add a bio to tell others about yourself"}
+
+
+ )}
+
+
+
+
+
+ {authMethod === "metamask" ? (
+
+ ) : (
+
+ )}
+
+
Auth Method
+
+ {authMethod === "metamask" ? "MetaMask Wallet" : "Email Account"}
+
+
+
+
+
+
+
+
+
+
{stats.hoursLearned}
+
Hours Learned
+
+
+
+
{stats.certificatesEarned}
+
Certificates
+
+
+
+
+
+
+ {/* Recent Activity */}
+
+
+
+
Recent Activity
+
+ View all →
+
+
+
+
+ {[
+ {
+ type: "course",
+ title: "Completed React Fundamentals",
+ time: "2 hours ago",
+ icon: BookOpen,
+ color: "green",
+ bgColor: "bg-green-100",
+ textColor: "text-green-600"
+ },
+ {
+ type: "quiz",
+ title: "Scored 95% on JavaScript Quiz",
+ time: "1 day ago",
+ icon: Award,
+ color: "blue",
+ bgColor: "bg-blue-100",
+ textColor: "text-blue-600"
+ },
+ {
+ type: "streak",
+ title: "7-day learning streak!",
+ time: "Today",
+ icon: Target,
+ color: "orange",
+ bgColor: "bg-orange-100",
+ textColor: "text-orange-600"
+ },
+ {
+ type: "rank",
+ title: "Moved up 5 positions in leaderboard",
+ time: "2 days ago",
+ icon: TrendingUp,
+ color: "purple",
+ bgColor: "bg-purple-100",
+ textColor: "text-purple-600"
+ },
+ ].map((activity, index) => (
+
+
+
+
{activity.title}
+
{activity.time}
+
+
+
+ ))}
+
+
+
+
🚀 Keep Learning!
+
+ You're doing great! Complete 2 more courses this week to maintain your streak.
+
+
+
+
+
+
+
+ )
}
diff --git a/frontend/components/ErrorBoundary.tsx b/frontend/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..0350629
--- /dev/null
+++ b/frontend/components/ErrorBoundary.tsx
@@ -0,0 +1,26 @@
+// components/ErrorBoundary.tsx
+import React from 'react'
+
+class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = { hasError: false }
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true }
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return
Something went wrong. Please refresh the page.
+ }
+
+ return this.props.children
+ }
+}
+
+// Wrap your app
+
+
+
diff --git a/frontend/context/auth-context.tsx b/frontend/context/auth-context.tsx
index 6420b24..1606c20 100644
--- a/frontend/context/auth-context.tsx
+++ b/frontend/context/auth-context.tsx
@@ -5,7 +5,6 @@ import detectEthereumProvider from "@metamask/detect-provider"
import { ethers } from "ethers"
import { toast } from "react-hot-toast"
import api from "@/lib/api"
-import type { AuthNonceRequest, AuthNonceResponse, AuthVerifyRequest, AuthVerifyResponse, User } from "@/lib/types"
import { auth } from "@/lib/firebase"
import {
signInWithEmailAndPassword,
@@ -15,12 +14,24 @@ import {
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
+}
+
interface AuthContextType {
- user: User | null // MetaMask user
- firebaseUser: FirebaseUser | null // Firebase user
- token: string | null // JWT token from backend (only for MetaMask users)
+ user: User | null
+ firebaseUser: FirebaseUser | null
+ token: string | null
isLoadingAuth: boolean
authMethod: "metamask" | "firebase" | null
+ walletAddress: string | null
+ walletConnected: boolean
connectWallet: () => Promise
loginWithEmail: (email: string, password: string) => Promise
signupWithEmail: (email: string, password: string) => Promise
@@ -30,39 +41,39 @@ interface AuthContextType {
const AuthContext = createContext(undefined)
export function AuthProvider({ children }: { children: React.ReactNode }) {
- const [user, setUser] = useState(null) // For MetaMask user
- const [firebaseUser, setFirebaseUser] = useState(null) // For Firebase user
- const [token, setToken] = useState(null) // JWT token
+ const [user, setUser] = useState(null)
+ const [firebaseUser, setFirebaseUser] = useState(null)
+ const [token, setToken] = useState(null)
const [isLoadingAuth, setIsLoadingAuth] = useState(true)
const [authMethod, setAuthMethod] = useState<"metamask" | "firebase" | null>(null)
+ const [walletAddress, setWalletAddress] = useState(null)
+ const [walletConnected, setWalletConnected] = useState(false)
+ // Initialize auth state
useEffect(() => {
- // Check for MetaMask token
const storedToken = localStorage.getItem("openlearnx_jwt_token")
const storedUser = localStorage.getItem("openlearnx_user")
- if (storedToken && storedUser) {
+ const storedWallet = localStorage.getItem("openlearnx_wallet")
+
+ if (storedToken && storedUser && storedWallet) {
try {
setUser(JSON.parse(storedUser))
setToken(storedToken)
+ setWalletAddress(storedWallet)
+ setWalletConnected(true)
setAuthMethod("metamask")
} catch (error) {
- console.error("Failed to parse stored MetaMask user or token:", error)
- localStorage.removeItem("openlearnx_jwt_token")
- localStorage.removeItem("openlearnx_user")
+ localStorage.clear()
}
}
- // Listen for Firebase auth state changes
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
- if (currentUser) {
+ if (currentUser && authMethod !== "metamask") {
setFirebaseUser(currentUser)
setAuthMethod("firebase")
- } else {
+ } else if (!currentUser && authMethod === "firebase") {
setFirebaseUser(null)
- if (authMethod !== "metamask") {
- // Only clear if not already MetaMask authenticated
- setAuthMethod(null)
- }
+ setAuthMethod(null)
}
setIsLoadingAuth(false)
})
@@ -70,26 +81,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return () => unsubscribe()
}, [authMethod])
- const logout = useCallback(async () => {
- setUser(null)
- setFirebaseUser(null)
- setToken(null)
- setAuthMethod(null)
- localStorage.removeItem("openlearnx_jwt_token")
- localStorage.removeItem("openlearnx_user")
- try {
- await signOut(auth) // Sign out from Firebase
- } catch (error) {
- console.error("Error signing out from Firebase:", error)
- }
- toast.success("Logged out successfully!")
- }, [])
-
const connectWallet = useCallback(async () => {
setIsLoadingAuth(true)
+
try {
const provider = await detectEthereumProvider()
-
if (!provider) {
toast.error("MetaMask not detected. Please install it.")
return
@@ -97,64 +93,81 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const ethProvider = new ethers.BrowserProvider(provider as any)
const accounts = await ethProvider.send("eth_requestAccounts", [])
-
if (accounts.length === 0) {
toast.error("No accounts connected.")
return
}
- const walletAddress = accounts[0]
+ const walletAddr = accounts[0]
- // 1. Request Nonce
- const nonceResponse = await api.post("/api/auth/nonce", {
- wallet_address: walletAddress,
- } as AuthNonceRequest)
- const { nonce, message } = nonceResponse.data
+ // 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")
+ }
- // 2. Sign Message
+ const { message } = nonceResponse.data
+
+ // Sign message
const signer = await ethProvider.getSigner()
const signature = await signer.signMessage(message)
- // 3. Verify Signature
- const verifyResponse = await api.post("/api/auth/verify", {
- wallet_address: walletAddress,
+ // Verify signature
+ const verifyResponse = await api.post("/api/auth/verify", {
+ wallet_address: walletAddr,
signature,
message,
- } as AuthVerifyRequest)
+ })
if (verifyResponse.data.success) {
- const { token: newToken, user: newUser } = verifyResponse.data
- setToken(newToken)
- setUser(newUser)
- setFirebaseUser(null) // Clear Firebase user if MetaMask logs in
+ const { token, user } = verifyResponse.data
+
+ // Update states
+ setToken(token)
+ setUser(user)
+ setWalletAddress(walletAddr)
+ setWalletConnected(true)
+ setFirebaseUser(null)
setAuthMethod("metamask")
- localStorage.setItem("openlearnx_jwt_token", newToken)
- localStorage.setItem("openlearnx_user", JSON.stringify(newUser))
- toast.success(`Welcome, ${newUser.wallet_address.slice(0, 6)}...${newUser.wallet_address.slice(-4)}!`)
+
+ // 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 {
- toast.error("MetaMask authentication failed. Please try again.")
- logout()
+ throw new Error("Authentication failed")
}
} catch (error: any) {
- console.error("MetaMask authentication error:", error)
- toast.error(error.message || "Failed to connect wallet or authenticate.")
- logout()
+ console.error("MetaMask error:", error)
+ toast.error(error.message || "Failed to connect MetaMask")
} finally {
setIsLoadingAuth(false)
}
- }, [logout])
+ }, [])
const loginWithEmail = useCallback(async (email: string, password: string) => {
setIsLoadingAuth(true)
try {
await signInWithEmailAndPassword(auth, email, password)
- // Firebase user is set by onAuthStateChanged listener
- setUser(null) // Clear MetaMask user if Firebase logs in
- setToken(null) // Clear JWT token
+ setUser(null)
+ setToken(null)
+ setWalletAddress(null)
+ setWalletConnected(false)
toast.success("Logged in with email!")
} catch (error: any) {
- console.error("Firebase login error:", error)
- toast.error(error.message || "Failed to login with email.")
+ toast.error(error.message || "Email login failed")
+ throw error
} finally {
setIsLoadingAuth(false)
}
@@ -164,40 +177,57 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setIsLoadingAuth(true)
try {
await createUserWithEmailAndPassword(auth, email, password)
- // Firebase user is set by onAuthStateChanged listener
- setUser(null) // Clear MetaMask user if Firebase logs in
- setToken(null) // Clear JWT token
- toast.success("Signed up and logged in with email!")
+ toast.success("Account created!")
} catch (error: any) {
- console.error("Firebase signup error:", error)
- toast.error(error.message || "Failed to sign up with email.")
+ toast.error(error.message || "Signup failed")
+ throw error
} finally {
setIsLoadingAuth(false)
}
}, [])
- const contextValue = React.useMemo(
- () => ({
- user,
- firebaseUser,
- token,
- isLoadingAuth,
- authMethod,
- connectWallet,
- loginWithEmail,
- signupWithEmail,
- logout,
- }),
- [user, firebaseUser, token, isLoadingAuth, authMethod, connectWallet, loginWithEmail, signupWithEmail, logout],
- )
+ const logout = useCallback(async () => {
+ setUser(null)
+ setFirebaseUser(null)
+ setToken(null)
+ setWalletAddress(null)
+ setWalletConnected(false)
+ setAuthMethod(null)
+ localStorage.clear()
+
+ try {
+ await signOut(auth)
+ } catch (error) {
+ console.error("Logout error:", error)
+ }
+
+ toast.success("Logged out!")
+ }, [])
- return {children}
+ const value = {
+ user,
+ firebaseUser,
+ token,
+ isLoadingAuth,
+ authMethod,
+ walletAddress,
+ walletConnected,
+ connectWallet,
+ loginWithEmail,
+ signupWithEmail,
+ logout,
+ }
+
+ return {children}
}
export function useAuth() {
const context = useContext(AuthContext)
- if (context === undefined) {
+ if (!context) {
throw new Error("useAuth must be used within an AuthProvider")
}
return context
-}
\ No newline at end of file
+}
+
+// ✅ CRITICAL: Default export to fix the "invalid element type" error
+export default AuthProvider