Initial commit of CK-QR Platform
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
# Also install nodemon globally for dev hot-reloading
|
||||
RUN npm install -g nodemon
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4001
|
||||
EXPOSE 9229
|
||||
|
||||
CMD ["nodemon", "--inspect=0.0.0.0:9229", "server.js"]
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'free',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qr_codes (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(20) NOT NULL, -- 'static' or 'dynamic'
|
||||
data_type VARCHAR(50) NOT NULL, -- 'URL', 'Wi-Fi', etc.
|
||||
destination_url TEXT,
|
||||
short_url_id VARCHAR(50) UNIQUE, -- Only for dynamic routing e.g. 'xyz123'
|
||||
design_data JSONB, -- Custom styling
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS qr_scans (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
qr_code_id UUID REFERENCES qr_codes(id) ON DELETE CASCADE,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
os VARCHAR(100),
|
||||
device_type VARCHAR(50),
|
||||
scanned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT,
|
||||
category VARCHAR(50),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert some default keys so they show up in the UI
|
||||
INSERT INTO app_settings (key, value, category) VALUES
|
||||
('icici_merchant_id', '', 'payments'),
|
||||
('icici_api_key', '', 'payments'),
|
||||
('google_safe_browsing_key', '', 'security'),
|
||||
('google_client_id', '', 'auth'),
|
||||
('apple_service_id', '', 'auth')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
INSERT INTO users (email, password_hash, role)
|
||||
VALUES ('admin@ckqr.com', '$2b$10$ezY4oBvmZUc667XCKbl98.hKicJbOG9yaV9lFbZXwbeXWgMuN7L/6', 'enterprise')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
@@ -0,0 +1,22 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const verifyToken = (req, res, next) => {
|
||||
let token = req.headers['authorization'];
|
||||
|
||||
// Check cookies if no auth header
|
||||
if (!token && req.cookies && req.cookies.token) {
|
||||
token = req.cookies.token;
|
||||
} else if (token && token.startsWith('Bearer ')) {
|
||||
token = token.slice(7, token.length).trimLeft();
|
||||
}
|
||||
|
||||
if (!token) return res.status(403).json({ error: 'Token missing from header' });
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET || 'supersecret123', (err, decoded) => {
|
||||
if (err) return res.status(401).json({ error: 'Unauthorized' });
|
||||
req.user = decoded;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { verifyToken };
|
||||
Generated
+1740
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.22.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"redis": "^6.1.0",
|
||||
"shortid": "^2.2.17",
|
||||
"ua-parser-js": "^2.0.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// In a real app, you would have an admin middleware protecting this
|
||||
// const { verifyAdminToken } = require('../middleware/adminAuth');
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
// List all users
|
||||
router.get('/users', async (req, res) => {
|
||||
try {
|
||||
const result = await pgPool.query('SELECT id, email, role, created_at FROM users ORDER BY created_at DESC');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update a user's tier
|
||||
router.put('/users/:id/tier', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { role } = req.body; // 'free', 'pro', 'enterprise'
|
||||
|
||||
if (!['free', 'pro', 'enterprise'].includes(role)) {
|
||||
return res.status(400).json({ error: 'Invalid role specified' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pgPool.query(
|
||||
'UPDATE users SET role = $1 WHERE id = $2 RETURNING id, email, role',
|
||||
[role, id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'User tier updated successfully', user: result.rows[0] });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all API settings
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const result = await pgPool.query('SELECT * FROM app_settings ORDER BY category, key');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server Error fetching settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update multiple API settings
|
||||
router.put('/settings', async (req, res) => {
|
||||
const { settings } = req.body; // Array of { key, value }
|
||||
if (!Array.isArray(settings)) return res.status(400).json({ error: 'Invalid settings format' });
|
||||
|
||||
const client = await pgPool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
for (const setting of settings) {
|
||||
await client.query(
|
||||
`INSERT INTO app_settings (key, value)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = CURRENT_TIMESTAMP`,
|
||||
[setting.key, setting.value]
|
||||
);
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
res.json({ message: 'Settings updated successfully' });
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server Error updating settings' });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,61 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Pool } = require('pg');
|
||||
const { verifyToken } = require('../middleware/auth');
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
router.get('/:qrCodeId', verifyToken, async (req, res) => {
|
||||
const { qrCodeId } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
// Ensure this user owns the QR code
|
||||
const checkOwner = await pgPool.query('SELECT id FROM qr_codes WHERE id = $1 AND user_id = $2', [qrCodeId, userId]);
|
||||
if (checkOwner.rows.length === 0) {
|
||||
return res.status(403).json({ error: 'Unauthorized or QR code not found' });
|
||||
}
|
||||
|
||||
// Get total scans
|
||||
const totalQuery = await pgPool.query('SELECT COUNT(*) as total FROM qr_scans WHERE qr_code_id = $1', [qrCodeId]);
|
||||
|
||||
// Get OS breakdown
|
||||
const osQuery = await pgPool.query(
|
||||
'SELECT os, COUNT(*) as count FROM qr_scans WHERE qr_code_id = $1 GROUP BY os',
|
||||
[qrCodeId]
|
||||
);
|
||||
|
||||
// Get Device Type breakdown
|
||||
const deviceQuery = await pgPool.query(
|
||||
'SELECT device_type, COUNT(*) as count FROM qr_scans WHERE qr_code_id = $1 GROUP BY device_type',
|
||||
[qrCodeId]
|
||||
);
|
||||
|
||||
// Get Scans over time (last 7 days grouped by date)
|
||||
const timeQuery = await pgPool.query(
|
||||
`SELECT DATE(scanned_at) as date, COUNT(*) as count
|
||||
FROM qr_scans
|
||||
WHERE qr_code_id = $1 AND scanned_at >= NOW() - INTERVAL '7 days'
|
||||
GROUP BY DATE(scanned_at) ORDER BY date ASC`,
|
||||
[qrCodeId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
totalScans: parseInt(totalQuery.rows[0].total),
|
||||
osBreakdown: osQuery.rows,
|
||||
deviceBreakdown: deviceQuery.rows,
|
||||
timeline: timeQuery.rows
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server Error fetching analytics' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,86 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
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 JWT_SECRET = process.env.JWT_SECRET || 'supersecret123';
|
||||
|
||||
// Register
|
||||
router.post('/register', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
|
||||
|
||||
try {
|
||||
const checkUser = await pgPool.query('SELECT id FROM users WHERE email = $1', [email]);
|
||||
if (checkUser.rows.length > 0) return res.status(400).json({ error: 'User already exists' });
|
||||
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
const newUser = await pgPool.query(
|
||||
'INSERT INTO users (email, password_hash, role) VALUES ($1, $2, $3) RETURNING id, email, role',
|
||||
[email, hash, 'free_trial']
|
||||
);
|
||||
|
||||
const token = jwt.sign({ id: newUser.rows[0].id, email: newUser.rows[0].email, 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] });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post('/login', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
|
||||
|
||||
try {
|
||||
const userQuery = await pgPool.query('SELECT * FROM users WHERE email = $1', [email]);
|
||||
if (userQuery.rows.length === 0) return res.status(404).json({ error: 'User not found' });
|
||||
|
||||
const user = userQuery.rows[0];
|
||||
const isMatch = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isMatch) return res.status(401).json({ error: 'Invalid credentials' });
|
||||
|
||||
const token = jwt.sign({ id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: '14d' });
|
||||
res.cookie('token', token, { httpOnly: true, maxAge: 14 * 24 * 60 * 60 * 1000, secure: false, sameSite: 'lax' });
|
||||
|
||||
res.json({ user: { id: user.id, email: user.email, role: user.role } });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get Current User (Validate Session)
|
||||
const { verifyToken } = require('../middleware/auth');
|
||||
router.get('/me', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const userQuery = await pgPool.query('SELECT id, email, role FROM users WHERE id = $1', [req.user.id]);
|
||||
if (userQuery.rows.length === 0) return res.status(404).json({ error: 'User not found' });
|
||||
res.json({ user: userQuery.rows[0] });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout
|
||||
router.post('/logout', (req, res) => {
|
||||
res.clearCookie('token');
|
||||
res.json({ message: 'Logged out' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,29 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Placeholder for ICICI Payment Gateway initialization
|
||||
router.post('/icici/initiate', async (req, res) => {
|
||||
const { amount, plan, userId } = req.body;
|
||||
|
||||
// Here you would construct the ICICI request payload, compute checksums, etc.
|
||||
// and return the URL or parameters for the frontend to redirect the user to ICICI.
|
||||
|
||||
console.log(`Initiating ICICI payment for user ${userId}, plan: ${plan}, amount: ${amount}`);
|
||||
|
||||
res.json({
|
||||
message: 'Payment initiation logic goes here',
|
||||
redirectUrl: 'https://placeholder.icicibank.com/pay'
|
||||
});
|
||||
});
|
||||
|
||||
// Placeholder for ICICI Webhook/Callback
|
||||
router.post('/icici/webhook', async (req, res) => {
|
||||
// Here ICICI will post back payment success or failure
|
||||
// We would verify the signature, and if successful, upgrade the user in the database.
|
||||
|
||||
console.log('Received ICICI Webhook:', req.body);
|
||||
|
||||
res.status(200).send('Webhook received');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,137 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const QRCode = require('qrcode');
|
||||
const shortid = require('shortid');
|
||||
const { verifyToken } = 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',
|
||||
});
|
||||
|
||||
// Get all QRs for user
|
||||
router.get('/', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const result = await pgPool.query(
|
||||
`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
|
||||
ORDER BY q.created_at DESC`,
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ qr_codes: result.rows });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error fetching QR codes' });
|
||||
}
|
||||
});
|
||||
|
||||
// Generate QR Code
|
||||
router.post('/generate', verifyToken, async (req, res) => {
|
||||
const { name, type, dataType, destinationUrl, designData, customSlug } = 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;
|
||||
|
||||
if (type === 'dynamic') {
|
||||
if (!destinationUrl) return res.status(400).json({ error: 'Destination URL is required for dynamic QR' });
|
||||
|
||||
// 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.BASE_URL || 'http://localhost:4001'}/l/${shortUrlId}`;
|
||||
}
|
||||
|
||||
const result = await pgPool.query(
|
||||
`INSERT INTO qr_codes (user_id, name, type, data_type, destination_url, short_url_id, design_data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
||||
[userId, name || 'Untitled QR', type, dataType, destinationUrl, shortUrlId, designData || {}]
|
||||
);
|
||||
|
||||
// Generate Image (Data URI for simple response, could be saved to S3 later)
|
||||
const qrImage = await QRCode.toDataURL(finalQrContent);
|
||||
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update QR Code (Editable Link)
|
||||
router.put('/:id', verifyToken, async (req, res) => {
|
||||
const { name, destinationUrl, shortUrlId } = 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),
|
||||
destination_url = COALESCE($2, destination_url),
|
||||
short_url_id = COALESCE($3, short_url_id)
|
||||
WHERE id = $4 RETURNING *`,
|
||||
[name, destinationUrl, newShortUrlId, 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, 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;
|
||||
@@ -0,0 +1,81 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Pool } = require('pg');
|
||||
const { createClient } = require('redis');
|
||||
const UAParser = require('ua-parser-js');
|
||||
|
||||
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 redisClient = createClient({
|
||||
url: `redis://${process.env.REDIS_HOST || 'redis'}:${process.env.REDIS_PORT || 6379}`
|
||||
});
|
||||
redisClient.on('error', (err) => console.log('Redis Client Error', err));
|
||||
(async () => {
|
||||
if (!redisClient.isOpen) await redisClient.connect();
|
||||
})();
|
||||
|
||||
router.get('/:shortUrlId', async (req, res) => {
|
||||
const { shortUrlId } = req.params;
|
||||
const cacheKey = `qr:${shortUrlId}:full`;
|
||||
|
||||
try {
|
||||
let qrData = null;
|
||||
|
||||
// 1. Check Cache
|
||||
const cachedData = await redisClient.get(cacheKey);
|
||||
if (cachedData) {
|
||||
qrData = JSON.parse(cachedData);
|
||||
} else {
|
||||
// 2. Not in cache, check DB
|
||||
const result = await pgPool.query(
|
||||
`SELECT q.id as qr_id, q.destination_url, u.role
|
||||
FROM qr_codes q
|
||||
JOIN users u ON q.user_id = u.id
|
||||
WHERE q.short_url_id = $1`,
|
||||
[shortUrlId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).send('QR Code not found');
|
||||
}
|
||||
|
||||
qrData = result.rows[0];
|
||||
await redisClient.setEx(cacheKey, 3600, JSON.stringify(qrData));
|
||||
}
|
||||
|
||||
// 3. Track Analytics (Async so it doesn't block redirection)
|
||||
const parser = new UAParser(req.headers['user-agent']);
|
||||
const os = parser.getOS().name || 'Unknown';
|
||||
const deviceType = parser.getDevice().type || 'desktop';
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
|
||||
pgPool.query(
|
||||
`INSERT INTO qr_scans (qr_code_id, ip_address, user_agent, os, device_type)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[qrData.qr_id, ip, req.headers['user-agent'], os, deviceType]
|
||||
).catch(err => console.error('Failed to log scan:', err));
|
||||
|
||||
// 4. Check Tier & Redirect
|
||||
if (qrData.role === 'free') {
|
||||
const encodedUrl = encodeURIComponent(qrData.destination_url);
|
||||
// In production, this points to the real Next.js domain
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:4000';
|
||||
return res.redirect(302, `${frontendUrl}/ad-redirect?url=${encodedUrl}`);
|
||||
} else {
|
||||
// Paid users skip the ad
|
||||
return res.redirect(302, qrData.destination_url);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Server Error during redirection');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,78 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { Pool } = require('pg');
|
||||
const { createClient } = require('redis');
|
||||
const cookieParser = require('cookie-parser');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 4001;
|
||||
|
||||
app.use(cors({ origin: 'http://localhost:4000', credentials: true }));
|
||||
app.use(express.json());
|
||||
|
||||
// Routes
|
||||
const authRoutes = require('./routes/auth');
|
||||
const qrRoutes = require('./routes/qr');
|
||||
const redirectRoutes = require('./routes/redirect');
|
||||
const analyticsRoutes = require('./routes/analytics');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
const paymentsRoutes = require('./routes/payments');
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/qr', qrRoutes);
|
||||
app.use('/api/analytics', analyticsRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/payments', paymentsRoutes);
|
||||
app.use('/l', redirectRoutes);
|
||||
|
||||
// Postgres setup
|
||||
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',
|
||||
});
|
||||
|
||||
// Redis setup
|
||||
const redisClient = createClient({
|
||||
url: `redis://${process.env.REDIS_HOST || 'redis'}:${process.env.REDIS_PORT || 6379}`
|
||||
});
|
||||
|
||||
redisClient.on('error', (err) => console.log('Redis Client Error', err));
|
||||
|
||||
app.get('/api/health', async (req, res) => {
|
||||
let pgStatus = 'Unknown';
|
||||
let redisStatus = 'Unknown';
|
||||
|
||||
try {
|
||||
const pgRes = await pgPool.query('SELECT NOW()');
|
||||
pgStatus = 'Connected';
|
||||
} catch (err) {
|
||||
pgStatus = `Error: ${err.message}`;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!redisClient.isOpen) {
|
||||
await redisClient.connect();
|
||||
}
|
||||
await redisClient.ping();
|
||||
redisStatus = 'Connected';
|
||||
} catch (err) {
|
||||
redisStatus = `Error: ${err.message}`;
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
services: {
|
||||
api: 'Running',
|
||||
postgres: pgStatus,
|
||||
redis: redisStatus
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`CK-QR API listening on port ${port}`);
|
||||
});
|
||||
Reference in New Issue
Block a user