Integrated real database authentication with JWT tokens
- Updated auth verification to use Neon database instead of mock data - Implemented proper JWT token authentication with localStorage storage - Created beautiful login page with admin quick-login for development - Updated all admin auth hooks to use JWT tokens from localStorage - Added automatic token cleanup on authentication failures - Enhanced AdminProtected component with proper token validation - Created logout functionality that clears tokens and redirects - Maintained fallback admin access for development (no token = admin) - Real admin credentials: admin@tcgvault.com / admin123 - Seamless integration with existing admin card editor workflow
This commit is contained in:
parent
0d6c6f1957
commit
53423509f0
5 changed files with 275 additions and 102 deletions
|
|
@ -14,7 +14,19 @@ export default function AdminProtected({ children }) {
|
|||
|
||||
const checkAdminAccess = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify');
|
||||
// Get token from localStorage
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Add authorization header if token exists
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/verify', { headers });
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
if (userData.role === 'admin') {
|
||||
|
|
@ -25,6 +37,10 @@ export default function AdminProtected({ children }) {
|
|||
}
|
||||
} else {
|
||||
setAccessDenied(true);
|
||||
// Clear invalid token
|
||||
if (token) {
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Admin auth check failed:', error);
|
||||
|
|
|
|||
|
|
@ -13,12 +13,28 @@ export function AdminProvider({ children }) {
|
|||
|
||||
const checkAdminAuth = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify');
|
||||
// Get token from localStorage
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Add authorization header if token exists
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/verify', { headers });
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
setUser(userData);
|
||||
} else {
|
||||
setUser(null);
|
||||
// Clear invalid token
|
||||
if (token) {
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
|
|
@ -67,12 +83,28 @@ export function useIsAdmin() {
|
|||
useEffect(() => {
|
||||
const checkAdmin = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify');
|
||||
// Get token from localStorage
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Add authorization header if token exists
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/verify', { headers });
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
setIsAdmin(userData.role === 'admin');
|
||||
} else {
|
||||
setIsAdmin(false);
|
||||
// Clear invalid token
|
||||
if (token) {
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setIsAdmin(false);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { verifyToken, getUserById } from '../auth-utils.js';
|
||||
import { sql } from '@vercel/postgres';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
// Set CORS headers
|
||||
|
|
@ -17,20 +20,50 @@ export default async function handler(req, res) {
|
|||
}
|
||||
|
||||
try {
|
||||
// For development purposes, return mock admin user
|
||||
// In production, implement proper JWT/session verification
|
||||
const mockAdminUser = {
|
||||
id: 1,
|
||||
email: 'admin@tcgvault.com',
|
||||
role: 'admin',
|
||||
name: 'Admin User',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
// For development, return admin user if no token provided
|
||||
// In production, this should return 401
|
||||
const result = await sql`
|
||||
SELECT id, email, role, created_at
|
||||
FROM users
|
||||
WHERE email = 'admin@tcgvault.com'
|
||||
`;
|
||||
|
||||
res.status(200).json(mockAdminUser);
|
||||
if (result.rows.length > 0) {
|
||||
return res.status(200).json(result.rows[0]);
|
||||
} else {
|
||||
return res.status(401).json({ error: 'No admin user found' });
|
||||
}
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// Get user data from database
|
||||
const result = await sql`
|
||||
SELECT id, email, role, created_at
|
||||
FROM users
|
||||
WHERE id = ${decoded.userId}
|
||||
`;
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
res.status(200).json(user);
|
||||
|
||||
} catch (jwtError) {
|
||||
console.error('JWT verification error:', jwtError);
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
console.error('Auth verification error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
236
pages/login.js
236
pages/login.js
|
|
@ -1,132 +1,208 @@
|
|||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAuth } from '../lib/auth-context.js';
|
||||
import Layout from '../components/Layout';
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isRegister, setIsRegister] = useState(false);
|
||||
|
||||
const { login, register } = useAuth();
|
||||
const router = useRouter();
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
// Clear error when user starts typing
|
||||
if (error) setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const result = isRegister
|
||||
? await register(email, password)
|
||||
: await login(email, password);
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
router.push('/dashboard');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Store the JWT token in localStorage
|
||||
localStorage.setItem('auth_token', data.token);
|
||||
|
||||
// Redirect to admin panel or dashboard
|
||||
if (data.user.role === 'admin') {
|
||||
router.push('/admin/card-editor');
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
} else {
|
||||
setError(result.error);
|
||||
setError(data.error || 'Login failed');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('An unexpected error occurred');
|
||||
console.error('Login error:', error);
|
||||
setError('Network error. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickAdminLogin = () => {
|
||||
setFormData({
|
||||
email: 'admin@tcgvault.com',
|
||||
password: 'admin123'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
{isRegister ? 'Create your account' : 'Sign in to your account'}
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
{isRegister ? 'Already have an account?' : "Don't have an account?"}{' '}
|
||||
<button
|
||||
onClick={() => setIsRegister(!isRegister)}
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
{isRegister ? 'Sign in' : 'Sign up'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<Layout user={null}>
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div className="text-center">
|
||||
<div className="text-6xl mb-4">🃏</div>
|
||||
<h2 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
Sign in to TCG Vault
|
||||
</h2>
|
||||
<p className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Access your trading card collection
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-8 rounded-2xl shadow-lg" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="p-4 rounded-lg bg-red-100 border border-red-200">
|
||||
<p className="text-red-700 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="input-field"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-lg border transition-all duration-200 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="input-field"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-lg border transition-all duration-200 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Loading...' : (isRegister ? 'Create Account' : 'Sign In')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full px-4 py-3 rounded-lg font-medium transition-all duration-200 ${
|
||||
loading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
Signing in...
|
||||
</div>
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isRegister && (
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">Demo Account</span>
|
||||
{/* Quick Admin Login for Development */}
|
||||
<div className="mt-6 pt-6 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="text-center">
|
||||
<p className="text-sm mb-3" style={{ color: 'var(--text-secondary)' }}>
|
||||
Development Quick Login:
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleQuickAdminLogin}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium border transition-all duration-200 hover:shadow-md"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
>
|
||||
🔧 Fill Admin Credentials
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 text-center text-sm text-gray-600">
|
||||
<p>Email: admin@tcgvault.com</p>
|
||||
<p>Password: admin123</p>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Don't have an account?{' '}
|
||||
<button
|
||||
onClick={() => router.push('/register')}
|
||||
className="font-medium hover:underline"
|
||||
style={{ color: 'var(--text-accent)' }}
|
||||
>
|
||||
Sign up
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Info Card */}
|
||||
<div className="p-4 rounded-xl border" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}>
|
||||
<div className="text-center">
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||
🛡️ Admin Access
|
||||
</h3>
|
||||
<div className="text-xs space-y-1" style={{ color: 'var(--text-secondary)' }}>
|
||||
<p><strong>Email:</strong> admin@tcgvault.com</p>
|
||||
<p><strong>Password:</strong> admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +1,36 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from '../components/Layout';
|
||||
|
||||
export default function Logout() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// In a real app, you would clear auth tokens here
|
||||
router.push('/login');
|
||||
// Clear the auth token
|
||||
localStorage.removeItem('auth_token');
|
||||
|
||||
// Redirect to login after a short delay
|
||||
setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 2000);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Logging out...</p>
|
||||
<Layout user={null}>
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl mb-4">👋</div>
|
||||
<h2 className="text-2xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||
Signing you out...
|
||||
</h2>
|
||||
<p style={{ color: 'var(--text-secondary)' }}>
|
||||
You will be redirected to the login page shortly.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 mx-auto" style={{ borderColor: 'var(--text-accent)' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue