feat(auth): Enforce global email verification for all users, restrict dashboard if unverified, add verification badges and banners
This commit is contained in:
@@ -6,6 +6,17 @@ import { usePathname } from 'next/navigation';
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [user, setUser] = useState<any>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
import('@/lib/api').then(({ default: api }) => {
|
||||
api.get('/auth/me').then(res => {
|
||||
setUser(res.data.user);
|
||||
}).catch(err => {
|
||||
console.error('Failed to fetch user', err);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const menuItems = [
|
||||
{ name: 'My QR Codes', path: '/dashboard', icon: 'M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' },
|
||||
@@ -38,10 +49,17 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<Link href="/dashboard/create" className="flex items-center justify-center gap-2 w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-xl transition-colors shadow-sm mb-6">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
||||
Create New QR Code
|
||||
</Link>
|
||||
{user?.is_email_verified === false ? (
|
||||
<button disabled className="flex items-center justify-center gap-2 w-full bg-gray-400 text-white font-bold py-3 px-4 rounded-xl shadow-sm mb-6 cursor-not-allowed opacity-70">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
||||
Verify Email to Create
|
||||
</button>
|
||||
) : (
|
||||
<Link href="/dashboard/create" className="flex items-center justify-center gap-2 w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-xl transition-colors shadow-sm mb-6">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
||||
Create New QR Code
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-4 space-y-1 overflow-y-auto">
|
||||
@@ -75,8 +93,25 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 h-screen overflow-y-auto relative">
|
||||
{children}
|
||||
<main className="flex-1 h-screen overflow-y-auto relative flex flex-col">
|
||||
{user?.is_email_verified === false && (
|
||||
<div className="bg-red-50 border-b border-red-200 px-6 py-3 flex items-center justify-between shadow-sm z-10">
|
||||
<div className="flex items-center gap-3 text-red-800">
|
||||
<svg className="w-6 h-6 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<p className="font-medium text-sm">
|
||||
Your account is currently restricted. Please verify your email address to unlock QR Code creation.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/dashboard/profile" className="text-sm font-bold bg-white text-red-700 hover:bg-red-100 border border-red-300 px-4 py-1.5 rounded-lg transition-colors whitespace-nowrap">
|
||||
Verify Now
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [user, setUser] = useState<{ email: string; role: string } | null>(null);
|
||||
const [user, setUser] = useState<{ email: string; role: string; is_email_verified: boolean } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,11 +37,40 @@ export default function ProfilePage() {
|
||||
<div className="w-20 h-20 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center text-3xl font-bold uppercase shadow-sm">
|
||||
{user.email.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{user.email}</h2>
|
||||
<span className="inline-flex items-center mt-2 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-indigo-50 text-indigo-700 border border-indigo-100">
|
||||
{user.role.replace('_', ' ')} Plan
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-2xl font-bold text-gray-900">{user.email}</h2>
|
||||
{user.is_email_verified ? (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg>
|
||||
Verified
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
Unverified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-indigo-50 text-indigo-700 border border-indigo-100">
|
||||
{user.role.replace('_', ' ')} Plan
|
||||
</span>
|
||||
{!user.is_email_verified && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await api.post('/auth/send-verification');
|
||||
alert('Verification email sent! Please check your inbox.');
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.error || 'Failed to send email');
|
||||
}
|
||||
}}
|
||||
className="text-sm font-semibold text-indigo-600 hover:text-indigo-800 underline"
|
||||
>
|
||||
Resend Verification Email
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user