const express = require('express'); const cors = require('cors'); const { Pool } = require('pg'); const { createClient } = require('redis'); const cookieParser = require('cookie-parser'); const fs = require('fs'); const path = require('path'); require('dotenv').config(); const app = express(); const port = process.env.PORT || 4001; const allowedOrigins = [ 'http://localhost:4000', 'https://qr.houseofwebsites.ai', process.env.FRONTEND_URL ].filter(Boolean); app.use(cors({ origin: allowedOrigins, credentials: true })); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ limit: '50mb', extended: true })); app.use(cookieParser()); // 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'); const foldersRoutes = require('./routes/folders'); 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('/api/folders', foldersRoutes); // Support Traefik stripped prefixes app.use('/auth', authRoutes); app.use('/qr', qrRoutes); app.use('/analytics', analyticsRoutes); app.use('/admin', adminRoutes); app.use('/payments', paymentsRoutes); app.use('/folders', foldersRoutes); 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', }); // Initialize Database automatically const initDb = async () => { try { const sql = fs.readFileSync(path.join(__dirname, 'db', 'init.sql'), 'utf8'); await pgPool.query(sql); console.log('Database initialized successfully.'); // Force promote the owner to admin (enterprise role) await pgPool.query("UPDATE users SET role = 'enterprise', is_email_verified = true WHERE email = 'mohan.k@cavininfotech.com'"); await pgPool.query("UPDATE users SET role = 'enterprise', is_email_verified = true WHERE email = 'admin@cavininfotech.com'"); await pgPool.query("UPDATE users SET role = 'enterprise', is_email_verified = true WHERE role = 'admin'"); } catch (err) { console.error('Database initialization failed:', err); } }; initDb(); // 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)); const healthCheck = 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.get('/api/health', healthCheck); app.get('/health', healthCheck); // Auto-migrate schema pgPool.query(` CREATE TABLE IF NOT EXISTS folders ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );`).catch(console.error); pgPool.query(`ALTER TABLE qr_codes ADD COLUMN IF NOT EXISTS name VARCHAR(255);`).catch(console.error); pgPool.query(`ALTER TABLE qr_codes ADD COLUMN IF NOT EXISTS folder_id UUID REFERENCES folders(id) ON DELETE SET NULL;`).catch(console.error); pgPool.query(` 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(50), device_type VARCHAR(50), browser VARCHAR(50), country VARCHAR(100), city VARCHAR(100), scanned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );`).catch(console.error); pgPool.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS is_email_verified BOOLEAN DEFAULT false, ADD COLUMN IF NOT EXISTS email_verification_token VARCHAR(255); `).catch(console.error); pgPool.query(`ALTER TABLE qr_codes ADD COLUMN IF NOT EXISTS tags TEXT[] DEFAULT '{}';`).catch(console.error); pgPool.query(`ALTER TABLE qr_codes ADD COLUMN IF NOT EXISTS routing_data JSONB;`).catch(console.error); pgPool.query(`ALTER TABLE qr_codes ADD COLUMN IF NOT EXISTS workspace_id UUID;`).catch(console.error); pgPool.query(`ALTER TABLE folders ADD COLUMN IF NOT EXISTS workspace_id UUID;`).catch(console.error); pgPool.query(` CREATE TABLE IF NOT EXISTS workspace_members ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), owner_id UUID REFERENCES users(id) ON DELETE CASCADE, member_email VARCHAR(255) NOT NULL, role VARCHAR(50) DEFAULT 'editor', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, UNIQUE(owner_id, member_email) ); `).catch(console.error); pgPool.query(` 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 ); `).then(() => { return pgPool.query(` 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'), ('smtp_host', 'smtp.office365.com', 'smtp'), ('smtp_port', '587', 'smtp'), ('smtp_user', 'noreply@cavininfotech.com', 'smtp'), ('smtp_pass', 'xmkshjlszjbkcyzb', 'smtp'), ('smtp_secure', 'false', 'smtp') ON CONFLICT (key) DO NOTHING; `); }).catch(console.error); app.listen(port, () => { console.log(`CK-QR API listening on port ${port}`); });