const express = require('express'); const router = express.Router(); const QRCode = require('qrcode'); const shortid = require('shortid'); const { verifyToken, requireEmailVerification } = require('../middleware/auth'); 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', }); // === BULK OPERATIONS === // POST /api/qr/bulk-delete router.post('/bulk-delete', verifyToken, async (req, res) => { const { ids } = req.body; if (!ids || !Array.isArray(ids) || ids.length === 0) { return res.status(400).json({ error: 'No IDs provided' }); } try { const result = await pgPool.query( 'DELETE FROM qr_codes WHERE id = ANY($1) AND user_id = $2 RETURNING id', [ids, req.user.id] ); res.json({ success: true, deleted: result.rowCount }); } catch (err) { console.error('Error in bulk delete:', err); res.status(500).json({ error: 'Server error during bulk delete' }); } }); // POST /api/qr/bulk-move router.post('/bulk-move', verifyToken, async (req, res) => { const { ids, folderId } = req.body; if (!ids || !Array.isArray(ids) || ids.length === 0) { return res.status(400).json({ error: 'No IDs provided' }); } try { let targetFolder = folderId; if (targetFolder === 'default') targetFolder = null; // Verify folder ownership if not default if (targetFolder) { const folderRes = await pgPool.query('SELECT id FROM folders WHERE id = $1 AND user_id = $2', [targetFolder, req.user.id]); if (folderRes.rows.length === 0) { return res.status(403).json({ error: 'Folder access denied' }); } } const result = await pgPool.query( 'UPDATE qr_codes SET folder_id = $1 WHERE id = ANY($2) AND user_id = $3 RETURNING id', [targetFolder, ids, req.user.id] ); res.json({ success: true, moved: result.rowCount }); } catch (err) { console.error('Error in bulk move:', err); res.status(500).json({ error: 'Server error during bulk move' }); } }); // Get all QRs for user router.get('/', verifyToken, async (req, res) => { try { const { folderId } = req.query; let queryStr = `SELECT q.*, COALESCE((SELECT COUNT(*) FROM qr_scans s WHERE s.qr_code_id = q.id), 0) as scans FROM qr_codes q WHERE q.user_id = $1`; let params = [req.user.id]; if (folderId === 'default') { queryStr += ` AND q.folder_id IS NULL`; } else if (folderId) { queryStr += ` AND q.folder_id = $2`; params.push(folderId); } queryStr += ` ORDER BY q.created_at DESC`; const result = await pgPool.query(queryStr, params); res.json({ qr_codes: result.rows }); } catch (err) { console.error(err); res.status(500).json({ error: 'Server error fetching QR codes' }); } }); // Get single QR code by ID router.get('/:id', verifyToken, async (req, res) => { try { const result = await pgPool.query( `SELECT * FROM qr_codes WHERE id = $1 AND user_id = $2`, [req.params.id, req.user.id] ); if (result.rows.length === 0) return res.status(404).json({ error: 'QR Code not found' }); res.json({ qr_code: result.rows[0] }); } catch (err) { console.error(err); res.status(500).json({ error: 'Server error' }); } }); // Generate QR Code router.post('/generate', verifyToken, requireEmailVerification, async (req, res) => { const { name, type, dataType, destinationUrl, routingData, designData, customSlug, folderId, tags } = req.body; const userId = req.user.id; if (!type || !dataType) { return res.status(400).json({ error: 'Missing type or dataType' }); } try { let shortUrlId = null; let finalQrContent = destinationUrl; // Only valid if static and URL // In static mode, the finalQrContent is whatever payload we send (handled mostly by frontend), // but if dynamic, we generate the shortUrlId. if (type === 'dynamic') { // Allow custom slug or generate random shortUrlId = customSlug ? customSlug.trim() : shortid.generate(); // Enforce uniqueness for custom slug if (customSlug) { const existing = await pgPool.query('SELECT id FROM qr_codes WHERE short_url_id = $1', [shortUrlId]); if (existing.rows.length > 0) { return res.status(409).json({ error: 'That custom link is already taken. Please choose another.' }); } } // In production, this would use the real domain (e.g. https://ck-qr.com/l/) finalQrContent = `${process.env.FRONTEND_URL || 'http://localhost:4000'}/l/${shortUrlId}`; } else { // If static, destinationUrl is sent as the actual payload by the frontend for simple URLs, // but for complex types, frontend puts the raw payload in designData.data. // We just store destinationUrl for backward compatibility. } const result = await pgPool.query( `INSERT INTO qr_codes (user_id, name, type, data_type, destination_url, routing_data, short_url_id, design_data, folder_id, tags) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`, [userId, name || 'Untitled QR', type, dataType, destinationUrl || null, routingData || null, shortUrlId, designData || {}, folderId || null, tags || []] ); // Generate Image (Data URI for simple response, could be saved to S3 later) const qrImage = await QRCode.toDataURL(finalQrContent || 'preview'); res.json({ message: 'QR Code generated', qrRecord: result.rows[0], qrImage: qrImage }); } catch (err) { console.error(err); res.status(500).json({ error: 'Server error generating QR code', details: err.message, stack: err.stack }); } }); // Update QR Code (Editable Link / Move Folder / Update Design) router.put('/:id', verifyToken, requireEmailVerification, async (req, res) => { const { name, dataType, destinationUrl, routingData, shortUrlId, designData, folderId, tags } = req.body; const qrId = req.params.id; const userId = req.user.id; try { // Verify ownership const qrCheck = await pgPool.query('SELECT * FROM qr_codes WHERE id = $1 AND user_id = $2', [qrId, userId]); if (qrCheck.rows.length === 0) { return res.status(404).json({ error: 'QR Code not found or unauthorized' }); } const qr = qrCheck.rows[0]; let newShortUrlId = shortUrlId ? shortUrlId.trim() : qr.short_url_id; // Check if slug changed and enforce uniqueness if (newShortUrlId !== qr.short_url_id) { const existing = await pgPool.query('SELECT id FROM qr_codes WHERE short_url_id = $1', [newShortUrlId]); if (existing.rows.length > 0) { return res.status(409).json({ error: 'That custom link is already taken. Please choose another.' }); } } // Update DB const updated = await pgPool.query( `UPDATE qr_codes SET name = COALESCE($1, name), data_type = COALESCE($2, data_type), destination_url = $3, routing_data = COALESCE($4, routing_data), short_url_id = COALESCE($5, short_url_id), design_data = COALESCE($6, design_data), folder_id = $7, tags = $8 WHERE id = $9 RETURNING *`, [ name, dataType, destinationUrl !== undefined ? destinationUrl : qr.destination_url, routingData !== undefined ? routingData : qr.routing_data, newShortUrlId, designData || qr.design_data, folderId !== undefined ? (folderId === 'default' ? null : folderId) : qr.folder_id, tags !== undefined ? tags : qr.tags, qrId ] ); res.json({ message: 'QR Code updated', qrRecord: updated.rows[0] }); } catch (err) { console.error(err); res.status(500).json({ error: 'Server error updating QR code' }); } }); // Delete QR Code 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' }); res.json({ message: 'QR Code deleted' }); } catch (err) { res.status(500).json({ error: 'Error deleting QR code' }); } }); module.exports = router;