diff --git a/api/middleware/auth.js b/api/middleware/auth.js index 32626d5..c36f0d0 100644 --- a/api/middleware/auth.js +++ b/api/middleware/auth.js @@ -1,4 +1,13 @@ const jwt = require('jsonwebtoken'); +const { Pool } = require('pg'); + +const pgPool = new Pool({ + host: process.env.PGHOST || 'postgres', + port: process.env.PGPORT || 5432, + user: process.env.PGUSER || 'postgres', + password: process.env.PGPASSWORD || 'postgres', + database: process.env.PGDATABASE || 'ckqr', +}); const verifyToken = (req, res, next) => { let token = req.headers['authorization']; @@ -19,4 +28,17 @@ const verifyToken = (req, res, next) => { }); }; -module.exports = { verifyToken }; +const requireEmailVerification = async (req, res, next) => { + try { + const userCheck = await pgPool.query('SELECT is_email_verified FROM users WHERE id = $1', [req.user.id]); + if (userCheck.rows.length === 0 || !userCheck.rows[0].is_email_verified) { + return res.status(403).json({ error: 'Email verification required' }); + } + next(); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error checking verification' }); + } +}; + +module.exports = { verifyToken, requireEmailVerification }; diff --git a/api/routes/auth.js b/api/routes/auth.js index cf5988f..7918a8f 100644 --- a/api/routes/auth.js +++ b/api/routes/auth.js @@ -37,7 +37,27 @@ router.post('/register', async (req, res) => { [email, hash, assignedRole] ); - const token = jwt.sign({ id: newUser.rows[0].id, email: newUser.rows[0].email, role: newUser.rows[0].role }, JWT_SECRET, { expiresIn: '14d' }); + const userId = newUser.rows[0].id; + const userEmail = newUser.rows[0].email; + + // Auto send verification email + const tokenStr = crypto.randomBytes(32).toString('hex'); + await pgPool.query('UPDATE users SET email_verification_token = $1 WHERE id = $2', [tokenStr, userId]); + + const baseUrl = process.env.FRONTEND_URL || 'https://qr.houseofwebsites.ai'; + const verifyLink = `${baseUrl}/verify-email?token=${tokenStr}`; + + const html = ` +

Verify Your Email

+

Thanks for registering for CK-QR! Please verify your email address to unlock your dashboard features.

+ Verify Email +

Or copy and paste this link:
${verifyLink}

+ `; + + // Send email asynchronously without blocking registration response + sendMail(userEmail, 'Verify your CK-QR account', html).catch(e => console.error('Failed to send auto-verify email:', e)); + + const token = jwt.sign({ id: userId, email: userEmail, role: newUser.rows[0].role }, JWT_SECRET, { expiresIn: '14d' }); res.cookie('token', token, { httpOnly: true, maxAge: 14 * 24 * 60 * 60 * 1000, secure: false, sameSite: 'lax' }); res.status(201).json({ message: 'User registered', user: newUser.rows[0] }); diff --git a/api/routes/qr.js b/api/routes/qr.js index d16cdd9..705b74e 100644 --- a/api/routes/qr.js +++ b/api/routes/qr.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const QRCode = require('qrcode'); const shortid = require('shortid'); -const { verifyToken } = require('../middleware/auth'); +const { verifyToken, requireEmailVerification } = require('../middleware/auth'); const { Pool } = require('pg'); const pgPool = new Pool({ @@ -56,7 +56,7 @@ router.get('/:id', verifyToken, async (req, res) => { }); // Generate QR Code -router.post('/generate', verifyToken, async (req, res) => { +router.post('/generate', verifyToken, requireEmailVerification, async (req, res) => { const { name, type, dataType, destinationUrl, designData, customSlug, folderId } = req.body; const userId = req.user.id; @@ -107,7 +107,7 @@ router.post('/generate', verifyToken, async (req, res) => { }); // Update QR Code (Editable Link / Move Folder / Update Design) -router.put('/:id', verifyToken, async (req, res) => { +router.put('/:id', verifyToken, requireEmailVerification, async (req, res) => { const { name, destinationUrl, shortUrlId, designData, folderId } = req.body; const qrId = req.params.id; const userId = req.user.id; @@ -150,7 +150,7 @@ router.put('/:id', verifyToken, async (req, res) => { }); // Delete QR Code -router.delete('/:id', verifyToken, async (req, res) => { +router.delete('/:id', verifyToken, requireEmailVerification, async (req, res) => { try { const result = await pgPool.query('DELETE FROM qr_codes WHERE id = $1 AND user_id = $2 RETURNING id', [req.params.id, req.user.id]); if (result.rows.length === 0) return res.status(404).json({ error: 'Not found' }); diff --git a/client/src/app/dashboard/layout.tsx b/client/src/app/dashboard/layout.tsx index 84d9717..15ac549 100644 --- a/client/src/app/dashboard/layout.tsx +++ b/client/src/app/dashboard/layout.tsx @@ -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(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
- - - Create New QR Code - + {user?.is_email_verified === false ? ( + + ) : ( + + + Create New QR Code + + )}