feat(auth): Introduce 5-day grace period for email verification before lockout
This commit is contained in:
+11
-3
@@ -30,9 +30,17 @@ const verifyToken = (req, res, next) => {
|
|||||||
|
|
||||||
const requireEmailVerification = async (req, res, next) => {
|
const requireEmailVerification = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const userCheck = await pgPool.query('SELECT is_email_verified FROM users WHERE id = $1', [req.user.id]);
|
const userCheck = await pgPool.query('SELECT is_email_verified, created_at FROM users WHERE id = $1', [req.user.id]);
|
||||||
if (userCheck.rows.length === 0 || !userCheck.rows[0].is_email_verified) {
|
if (userCheck.rows.length === 0) return res.status(403).json({ error: 'User not found' });
|
||||||
return res.status(403).json({ error: 'Email verification required' });
|
|
||||||
|
const user = userCheck.rows[0];
|
||||||
|
if (!user.is_email_verified) {
|
||||||
|
const createdAt = new Date(user.created_at);
|
||||||
|
const now = new Date();
|
||||||
|
const daysSinceCreation = (now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
if (daysSinceCreation > 5) {
|
||||||
|
return res.status(403).json({ error: 'Email verification grace period expired (5 days)' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ router.post('/login', async (req, res) => {
|
|||||||
const { verifyToken } = require('../middleware/auth');
|
const { verifyToken } = require('../middleware/auth');
|
||||||
router.get('/me', verifyToken, async (req, res) => {
|
router.get('/me', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userQuery = await pgPool.query('SELECT id, email, role, is_email_verified FROM users WHERE id = $1', [req.user.id]);
|
const userQuery = await pgPool.query('SELECT id, email, role, is_email_verified, created_at FROM users WHERE id = $1', [req.user.id]);
|
||||||
if (userQuery.rows.length === 0) return res.status(404).json({ error: 'User not found' });
|
if (userQuery.rows.length === 0) return res.status(404).json({ error: 'User not found' });
|
||||||
res.json({ user: userQuery.rows[0] });
|
res.json({ user: userQuery.rows[0] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -49,17 +49,25 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{user?.is_email_verified === false ? (
|
{(() => {
|
||||||
|
if (user?.is_email_verified === false) {
|
||||||
|
const daysSinceCreation = (new Date().getTime() - new Date(user.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
if (daysSinceCreation > 5) {
|
||||||
|
return (
|
||||||
<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">
|
<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>
|
<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
|
Verify Email to Create
|
||||||
</button>
|
</button>
|
||||||
) : (
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
<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">
|
<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>
|
<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
|
Create New QR Code
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 px-4 space-y-1 overflow-y-auto">
|
<nav className="flex-1 px-4 space-y-1 overflow-y-auto">
|
||||||
@@ -94,7 +102,11 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 h-screen overflow-y-auto relative flex flex-col">
|
<main className="flex-1 h-screen overflow-y-auto relative flex flex-col">
|
||||||
{user?.is_email_verified === false && (
|
{(() => {
|
||||||
|
if (user?.is_email_verified === false) {
|
||||||
|
const daysSinceCreation = (new Date().getTime() - new Date(user.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
if (daysSinceCreation > 5) {
|
||||||
|
return (
|
||||||
<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="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">
|
<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">
|
<svg className="w-6 h-6 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -108,7 +120,28 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
Verify Now
|
Verify Now
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
} else {
|
||||||
|
const daysLeft = Math.ceil(5 - daysSinceCreation);
|
||||||
|
return (
|
||||||
|
<div className="bg-yellow-50 border-b border-yellow-200 px-6 py-3 flex items-center justify-between shadow-sm z-10">
|
||||||
|
<div className="flex items-center gap-3 text-yellow-800">
|
||||||
|
<svg className="w-6 h-6 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<p className="font-medium text-sm">
|
||||||
|
Please verify your email address within {daysLeft} {daysLeft === 1 ? 'day' : 'days'} to prevent account restriction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link href="/dashboard/profile" className="text-sm font-bold bg-white text-yellow-700 hover:bg-yellow-100 border border-yellow-300 px-4 py-1.5 rounded-lg transition-colors whitespace-nowrap">
|
||||||
|
Verify Now
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user