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 () => {
|
const checkAdminAccess = async () => {
|
||||||
try {
|
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) {
|
if (response.ok) {
|
||||||
const userData = await response.json();
|
const userData = await response.json();
|
||||||
if (userData.role === 'admin') {
|
if (userData.role === 'admin') {
|
||||||
|
|
@ -25,6 +37,10 @@ export default function AdminProtected({ children }) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setAccessDenied(true);
|
setAccessDenied(true);
|
||||||
|
// Clear invalid token
|
||||||
|
if (token) {
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Admin auth check failed:', error);
|
console.error('Admin auth check failed:', error);
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,28 @@ export function AdminProvider({ children }) {
|
||||||
|
|
||||||
const checkAdminAuth = async () => {
|
const checkAdminAuth = async () => {
|
||||||
try {
|
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) {
|
if (response.ok) {
|
||||||
const userData = await response.json();
|
const userData = await response.json();
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
} else {
|
} else {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
// Clear invalid token
|
||||||
|
if (token) {
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth check failed:', error);
|
console.error('Auth check failed:', error);
|
||||||
|
|
@ -67,12 +83,28 @@ export function useIsAdmin() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAdmin = async () => {
|
const checkAdmin = async () => {
|
||||||
try {
|
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) {
|
if (response.ok) {
|
||||||
const userData = await response.json();
|
const userData = await response.json();
|
||||||
setIsAdmin(userData.role === 'admin');
|
setIsAdmin(userData.role === 'admin');
|
||||||
} else {
|
} else {
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
|
// Clear invalid token
|
||||||
|
if (token) {
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsAdmin(false);
|
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) {
|
export default async function handler(req, res) {
|
||||||
// Set CORS headers
|
// Set CORS headers
|
||||||
|
|
@ -17,20 +20,50 @@ export default async function handler(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For development purposes, return mock admin user
|
const authHeader = req.headers.authorization;
|
||||||
// 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()
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(mockAdminUser);
|
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'
|
||||||
|
`;
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('Token verification error:', error);
|
console.error('Auth verification error:', error);
|
||||||
res.status(500).json({ error: 'Internal server 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 { useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useAuth } from '../lib/auth-context.js';
|
import Layout from '../components/Layout';
|
||||||
|
|
||||||
export default function Login() {
|
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 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) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = isRegister
|
const response = await fetch('/api/auth/login', {
|
||||||
? await register(email, password)
|
method: 'POST',
|
||||||
: await login(email, password);
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
if (result.success) {
|
const data = await response.json();
|
||||||
router.push('/dashboard');
|
|
||||||
|
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 {
|
} else {
|
||||||
setError(result.error);
|
setError(data.error || 'Login failed');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError('An unexpected error occurred');
|
console.error('Login error:', error);
|
||||||
|
setError('Network error. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleQuickAdminLogin = () => {
|
||||||
|
setFormData({
|
||||||
|
email: 'admin@tcgvault.com',
|
||||||
|
password: 'admin123'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<Layout user={null}>
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<div className="max-w-md w-full space-y-8">
|
||||||
{isRegister ? 'Create your account' : 'Sign in to your account'}
|
<div>
|
||||||
</h2>
|
<div className="text-center">
|
||||||
<p className="mt-2 text-center text-sm text-gray-600">
|
<div className="text-6xl mb-4">🃏</div>
|
||||||
{isRegister ? 'Already have an account?' : "Don't have an account?"}{' '}
|
<h2 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
<button
|
Sign in to TCG Vault
|
||||||
onClick={() => setIsRegister(!isRegister)}
|
</h2>
|
||||||
className="font-medium text-blue-600 hover:text-blue-500"
|
<p className="mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
>
|
Access your trading card collection
|
||||||
{isRegister ? 'Sign in' : 'Sign up'}
|
</p>
|
||||||
</button>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="p-8 rounded-2xl shadow-lg" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||||||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
{error && (
|
||||||
{error && (
|
<div className="p-4 rounded-lg bg-red-100 border border-red-200">
|
||||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
<p className="text-red-700 text-sm">{error}</p>
|
||||||
{error}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="email" className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||||
Email address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
value={email}
|
value={formData.email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
className="input-field"
|
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"
|
placeholder="Enter your email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="password" className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
value={password}
|
value={formData.password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||||
className="input-field"
|
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"
|
placeholder="Enter your password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
className={`w-full px-4 py-3 rounded-lg font-medium transition-all duration-200 ${
|
||||||
>
|
loading
|
||||||
{loading ? 'Loading...' : (isRegister ? 'Create Account' : 'Sign In')}
|
? 'opacity-50 cursor-not-allowed'
|
||||||
</button>
|
: 'gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105'
|
||||||
</div>
|
}`}
|
||||||
</form>
|
>
|
||||||
|
{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 && (
|
{/* Quick Admin Login for Development */}
|
||||||
<div className="mt-6">
|
<div className="mt-6 pt-6 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||||
<div className="relative">
|
<div className="text-center">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<p className="text-sm mb-3" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<div className="w-full border-t border-gray-300" />
|
Development Quick Login:
|
||||||
</div>
|
</p>
|
||||||
<div className="relative flex justify-center text-sm">
|
<button
|
||||||
<span className="px-2 bg-white text-gray-500">Demo Account</span>
|
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>
|
</div>
|
||||||
<div className="mt-6 text-center text-sm text-gray-600">
|
</form>
|
||||||
<p>Email: admin@tcgvault.com</p>
|
|
||||||
<p>Password: admin123</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,36 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import Layout from '../components/Layout';
|
||||||
|
|
||||||
export default function Logout() {
|
export default function Logout() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// In a real app, you would clear auth tokens here
|
// Clear the auth token
|
||||||
router.push('/login');
|
localStorage.removeItem('auth_token');
|
||||||
|
|
||||||
|
// Redirect to login after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/login');
|
||||||
|
}, 2000);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<Layout user={null}>
|
||||||
<div className="text-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto"></div>
|
<div className="text-center">
|
||||||
<p className="mt-4 text-gray-600">Logging out...</p>
|
<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>
|
||||||
</div>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue