| import 'dotenv/config'; |
| import express from 'express'; |
| import cors from 'cors'; |
| import multer from 'multer'; |
| import path from 'path'; |
| import fs from 'fs'; |
| import { v4 as uuidv4 } from 'uuid'; |
| import crypto from 'crypto'; |
| import db, { initDB } from './db.js'; |
|
|
| |
| import { createServer } from 'http'; |
| import { Server } from 'socket.io'; |
|
|
| const app = express(); |
| const PORT = process.env.PORT || 7860; |
|
|
| |
| const httpServer = createServer(app); |
|
|
| import * as nsfwjs from 'nsfwjs'; |
| import sharp from 'sharp'; |
|
|
| let tf; |
| try { |
| tf = await import('@tensorflow/tfjs-node'); |
| } catch (e) { |
| tf = await import('@tensorflow/tfjs'); |
| console.warn("⚠ Running TensorFlow.js in pure JS mode. Install @tensorflow/tfjs-node for faster performance."); |
| } |
|
|
| |
| import morgan from 'morgan'; |
| app.use(cors()); |
| app.use(express.json()); |
| app.use(morgan('dev')); |
|
|
| const uploadDir = path.resolve('./server/uploads'); |
| const modelsDir = path.resolve('./server/models'); |
|
|
| |
| |
| if (!fs.existsSync(uploadDir)) { |
| fs.mkdirSync(uploadDir, { recursive: true }); |
| } |
| if (!fs.existsSync(modelsDir)) { |
| fs.mkdirSync(modelsDir, { recursive: true }); |
| } |
|
|
| |
| httpServer.on('error', (err) => { |
| if (err.code === 'EADDRINUSE') { |
| console.warn(`Port ${PORT} already in use, trying ${PORT + 1}`); |
| httpServer.listen(PORT + 1); |
| } else { |
| console.error('Server error:', err); |
| } |
| }); |
|
|
| app.use('/uploads', express.static(uploadDir)); |
| app.use('/models', express.static(modelsDir)); |
|
|
| |
| const distDir = path.resolve('./dist'); |
| if (fs.existsSync(distDir)) { |
| app.use(express.static(distDir)); |
| } |
|
|
| const storage = multer.diskStorage({ |
| destination: function (req, file, cb) { |
| cb(null, uploadDir); |
| }, |
| filename: function (req, file, cb) { |
| const ext = path.extname(file.originalname); |
| cb(null, uuidv4() + ext); |
| } |
| }); |
| const upload = multer({ storage: storage }); |
|
|
| |
| let nsfwModel = null; |
| const loadModel = async () => { |
| try { |
| |
| await new Promise(resolve => setTimeout(resolve, 500)); |
| |
| |
| |
| const isNodeBackend = tf.engine().backendName === 'tensorflow'; |
| let modelFileUrl; |
| |
| if (isNodeBackend) { |
| |
| |
| const absPath = path.resolve(modelsDir); |
| modelFileUrl = `file://${absPath.startsWith('/') ? '' : '/'}${absPath.replace(/\\/g, '/')}/`; |
| console.log("Loading NSFW Model via Native Engine (File IO)..."); |
| } else { |
| modelFileUrl = `http://127.0.0.1:${PORT}/models/`; |
| console.log("Loading NSFW Model via Pure JS Mode (HTTP Loopback)..."); |
| } |
|
|
| nsfwModel = await nsfwjs.load(modelFileUrl, { size: 224 }); |
| console.log("✓ Offline NSFW Security Engine Loaded"); |
| } catch (err) { |
| console.warn(`⚠ Local NSFW Model load failed (err: ${err.message}). Attempting CDN fallback...`); |
| try { |
| |
| nsfwModel = await nsfwjs.load('https://nsfwjs.com/model/', { size: 224 }); |
| console.log("✓ Online NSFW Security Engine Loaded (CDN Fallback)"); |
| } catch (cdnErr) { |
| console.warn("⚠ NSFW Model could not be loaded even from CDN. Content moderation will be disabled.", cdnErr.message); |
| } |
| } |
| }; |
|
|
| |
| const checkInappropriate = async (filePath) => { |
| if (!nsfwModel) return false; |
| try { |
| |
| |
| const { data, info } = await sharp(filePath) |
| .resize(224, 224) |
| .removeAlpha() |
| .raw() |
| .toBuffer({ resolveWithObject: true }); |
|
|
| |
| const image = tf.tensor3d(new Uint8Array(data), [info.height, info.width, 3]); |
| const predictions = await nsfwModel.classify(image); |
| image.dispose(); |
| |
| |
| return predictions.some(p => |
| ['Porn', 'Hentai', 'Sexy'].includes(p.className) && p.probability > 0.6 |
| ); |
| } catch (err) { |
| console.error("AI Analysis Error:", err); |
| return false; |
| } |
| }; |
|
|
| |
| app.post('/api/users', upload.single('selfie'), async (req, res) => { |
| try { |
| const { id: deviceId, deviceToken, name, role, contact, nearestCity, district, state, pincode, location } = req.body; |
| if (!deviceId || !name || !role || !contact || !nearestCity || !district || !state || !pincode || !location || !req.file) { |
| if (req.file) try { fs.unlinkSync(req.file.path); } catch(e){} |
| return res.status(400).json({ error: 'Missing required fields, location or selfie.' }); |
| } |
|
|
| |
| const isInappropriate = await checkInappropriate(req.file.path); |
| if (isInappropriate) { |
| try { fs.unlinkSync(req.file.path); } catch(e){} |
| return res.status(400).json({ error: 'Snapshot rejected: Inappropriate content detected by security engine.' }); |
| } |
|
|
| const { rows: matches } = await db.query(` |
| SELECT * FROM users |
| WHERE contact = $1 AND role = $2 AND name = $3 AND state = $4 AND district = $5 AND pincode = $6 |
| `, [contact, role, name, state, district, pincode]); |
| const existingProfile = matches[0]; |
|
|
| const compoundId = existingProfile ? existingProfile.id : `${deviceId}_${role}`; |
| const selfiePath = `/uploads/${req.file.filename}`; |
|
|
| if (!existingProfile) { |
| |
| const { rows: phoneCheck } = await db.query(`SELECT id FROM users WHERE contact = $1 AND role = $2`, [contact, role]); |
| if (phoneCheck.length > 0) { |
| return res.status(400).json({ error: 'A profile with this phone number exists with different details. Please verify your exact name and location details to restore it.' }); |
| } |
|
|
| await db.query(` |
| INSERT INTO users (id, name, role, "selfiePath", contact, "nearestCity", district, state, pincode, location, "deviceId", "deviceToken") |
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) |
| `, [compoundId, name, role, selfiePath, contact, nearestCity, district, state, pincode, location, deviceId, deviceToken]); |
| } else { |
| |
| if (existingProfile.isBlocked) { |
| return res.status(403).json({ error: 'Account blocked by admin.' }); |
| } |
| |
| |
| if (existingProfile.selfiePath) { |
| try { fs.unlinkSync(path.join(process.cwd(), 'server', existingProfile.selfiePath)); } catch(e){} |
| } |
|
|
| await db.query(` |
| UPDATE users SET name = $1, "selfiePath" = $2, "nearestCity" = $3, district = $4, state = $5, pincode = $6, location = $7, "deviceId" = $8, "deviceToken" = $9 |
| WHERE id = $10 |
| `, [name, selfiePath, nearestCity, district, state, pincode, location, deviceId, deviceToken, compoundId]); |
| } |
|
|
| res.status(200).json({ |
| id: compoundId, deviceId, name, role, contact, |
| nearestCity, district, state, pincode, |
| location: JSON.parse(location), |
| selfiePath, isBlocked: existingProfile ? existingProfile.isBlocked : 0 |
| }); |
| } catch (err) { |
| console.error(err); |
| res.status(500).json({ error: 'Failed to register.' }); |
| } |
| }); |
|
|
| app.get('/api/users/device/:deviceId', async (req, res) => { |
| try { |
| const { deviceId } = req.params; |
| const token = req.headers['x-device-token']; |
|
|
| const { rows: profiles } = await db.query(` |
| SELECT * FROM users |
| WHERE "deviceId" = $1 OR id LIKE $2 |
| `, [deviceId, `${deviceId}_%`]); |
|
|
| |
| const validProfiles = profiles.filter(p => !p.deviceToken || p.deviceToken === token); |
|
|
| res.json(validProfiles); |
| } catch (err) { |
| console.error(err); |
| res.status(500).json({ error: 'Failed to fetch device profiles' }); |
| } |
| }); |
|
|
| app.get('/api/listings', async (req, res) => { |
| try { |
| const { rows } = await db.query(` |
| SELECT listings.*, users."selfiePath" as "sellerSelfie" |
| FROM listings |
| JOIN users ON listings."sellerId" = users.id |
| WHERE listings.status != 'sold' |
| ORDER BY timestamp DESC |
| `); |
|
|
| const result = await Promise.all(rows.map(async (listing) => { |
| const { rows: images } = await db.query('SELECT "imagePath" FROM listing_images WHERE "listingId" = $1', [listing.id]); |
| return { |
| ...listing, |
| location: JSON.parse(listing.location), |
| nearestCity: listing.nearestCity || '', |
| district: listing.district || '', |
| state: listing.state || '', |
| pincode: listing.pincode || '', |
| images: images.map(img => img.imagePath) |
| }; |
| })); |
| res.json(result); |
| } catch (err) { |
| console.error(err); |
| res.status(500).json({ error: 'Failed to fetch' }); |
| } |
| }); |
|
|
| app.post('/api/listings', upload.array('images', 5), async (req, res) => { |
| try { |
| const { sellerId, sellerName, cropName, quantity, price, location, nearestCity, district, state, pincode } = req.body; |
| |
| |
| let parsedLocation; |
| try { |
| parsedLocation = JSON.parse(location); |
| if (!parsedLocation || typeof parsedLocation.lat !== 'number' || typeof parsedLocation.lng !== 'number') { |
| throw new Error('Invalid coordinates'); |
| } |
| } catch (e) { |
| if (req.files) req.files.forEach(f => { try { fs.unlinkSync(f.path); } catch(ev){} }); |
| return res.status(400).json({ error: 'Valid GPS Location is strictly mandatory for all listings.' }); |
| } |
|
|
| if (!sellerId || !cropName || !quantity || !location || !req.files || req.files.length === 0) { |
| if (req.files) req.files.forEach(f => { try { fs.unlinkSync(f.path); } catch(e){} }); |
| return res.status(400).json({ error: 'Missing fields' }); |
| } |
|
|
| |
| for (const file of req.files) { |
| const isInappropriate = await checkInappropriate(file.path); |
| if (isInappropriate) { |
| |
| req.files.forEach(f => { try { fs.unlinkSync(f.path); } catch(e){} }); |
| return res.status(400).json({ error: 'Listing rejected: One or more images contain inappropriate content.' }); |
| } |
| } |
|
|
| const listingId = uuidv4(); |
| const timestamp = Date.now(); |
|
|
| await db.query(` |
| INSERT INTO listings (id, "sellerId", "sellerName", "cropName", quantity, price, location, "nearestCity", district, state, pincode, timestamp) |
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) |
| `, [listingId, sellerId, sellerName, cropName, quantity, price, location, nearestCity, district, state, pincode, timestamp]); |
|
|
| const imagePaths = req.files.map(file => `/uploads/${file.filename}`); |
| |
| await Promise.all(imagePaths.map(p => |
| db.query(`INSERT INTO listing_images ("listingId", "imagePath") VALUES ($1, $2)`, [listingId, p]) |
| )); |
|
|
| const { rows: userRows } = await db.query(`SELECT "selfiePath" FROM users WHERE id = $1`, [sellerId]); |
| const user = userRows[0]; |
| |
| res.status(201).json({ |
| id: listingId, sellerId, sellerName, |
| sellerSelfie: user ? user.selfiePath : null, |
| cropName, quantity, price, location: JSON.parse(location), |
| timestamp, images: imagePaths |
| }); |
| } catch (err) { |
| if (req.files) req.files.forEach(f => { try { fs.unlinkSync(f.path); } catch(e){} }); |
| res.status(500).json({ error: 'Failed to create' }); |
| } |
| }); |
|
|
| |
| const io = new Server(httpServer, { |
| cors: { |
| origin: '*', |
| methods: ['GET', 'POST'] |
| } |
| }); |
|
|
| |
| const activeUsers = new Map(); |
|
|
| |
| app.patch('/api/listings/:id/sold', async (req, res) => { |
| try { |
| const { id } = req.params; |
| await db.query(`UPDATE listings SET status = 'sold' WHERE id = $1`, [id]); |
| |
| |
| io.emit('listing-updated', { id, status: 'sold' }); |
| |
| res.json({ success: true, id, status: 'sold' }); |
| } catch (err) { |
| console.error(err); |
| res.status(500).json({ error: 'Failed to update listing' }); |
| } |
| }); |
|
|
| |
| app.patch('/api/listings/:id/edit', async (req, res) => { |
| try { |
| const { id } = req.params; |
| const { cropName, quantity, price } = req.body; |
| |
| await db.query(`UPDATE listings SET "cropName" = $1, quantity = $2, price = $3 WHERE id = $4`, [cropName, quantity, price, id]); |
| |
| |
| io.emit('listing-edited', { id, cropName, quantity, price }); |
| |
| res.json({ success: true, id, cropName, quantity, price }); |
| } catch (err) { |
| console.error(err); |
| res.status(500).json({ error: 'Failed to edit listing' }); |
| } |
| }); |
|
|
| |
| app.post('/api/messages', async (req, res) => { |
| try { |
| const { id, senderId, receiverId, message, timestamp } = req.body; |
| const socketId = req.headers['x-socket-id']; |
|
|
| |
| const activeSocket = activeUsers.get(senderId ? senderId.split('_')[0] : null); |
| if (!activeSocket || activeSocket !== socketId) { |
| return res.status(403).json({ error: 'Messaging Identity verification failed.' }); |
| } |
|
|
| await db.query(`INSERT INTO messages (id, "senderId", "receiverId", message, timestamp) VALUES ($1, $2, $3, $4, $5)`, [id, senderId, receiverId, message, timestamp]); |
| res.json({ success: true, id }); |
| } catch (err) { |
| console.error(err); |
| res.status(500).json({ error: 'Failed to send message' }); |
| } |
| }); |
|
|
| app.get('/api/messages/history/:user1/:user2', async (req, res) => { |
| try { |
| const { user1, user2 } = req.params; |
| const { rows: history } = await db.query(` |
| SELECT * FROM messages |
| WHERE ("senderId" = $1 AND "receiverId" = $2) OR ("senderId" = $3 AND "receiverId" = $4) |
| ORDER BY timestamp ASC |
| `, [user1, user2, user2, user1]); |
| res.json(history); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed to fetch messages' }); |
| } |
| }); |
|
|
| app.get('/api/messages/inbox/:userId', async (req, res) => { |
| try { |
| const { userId } = req.params; |
| |
| const { rows: inbox } = await db.query(` |
| SELECT * FROM ( |
| SELECT m.*, |
| CASE WHEN m."senderId" = $1 THEN r.name ELSE s.name END as "contactName", |
| CASE WHEN m."senderId" = $1 THEN r."selfiePath" ELSE s."selfiePath" END as "contactSelfie", |
| CASE WHEN m."senderId" = $1 THEN r.id ELSE s.id END as "contactId", |
| ROW_NUMBER() OVER(PARTITION BY CASE WHEN m."senderId" = $1 THEN m."receiverId" ELSE m."senderId" END ORDER BY m.timestamp DESC) as rn |
| FROM messages m |
| JOIN users s ON m."senderId" = s.id |
| JOIN users r ON m."receiverId" = r.id |
| WHERE m."senderId" = $1 OR m."receiverId" = $1 |
| ) sub |
| WHERE rn = 1 |
| ORDER BY timestamp DESC |
| `, [userId]); |
| res.json(inbox); |
| } catch (err) { |
| console.error("Inbox fetch err:", err); |
| res.status(500).json({ error: 'Failed to fetch inbox' }); |
| } |
| }); |
|
|
| app.get('/api/messages/unread-count/:userId', async (req, res) => { |
| try { |
| const { rows } = await db.query(`SELECT COUNT(*) as count FROM messages WHERE "receiverId" = $1 AND "isRead" = 0`, [req.params.userId]); |
| res.json({ count: parseInt(rows[0].count) }); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed to fetch unread count' }); |
| } |
| }); |
|
|
| app.patch('/api/messages/read/:senderId/:receiverId', async (req, res) => { |
| try { |
| await db.query(`UPDATE messages SET "isRead" = 1 WHERE "senderId" = $1 AND "receiverId" = $2`, [req.params.senderId, req.params.receiverId]); |
| res.json({ success: true }); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed' }); |
| } |
| }); |
|
|
| |
| app.post('/api/support', async (req, res) => { |
| try { |
| const { senderId, message } = req.body; |
| if (!senderId || !message) return res.status(400).json({ error: 'Missing fields' }); |
| const ticketId = uuidv4(); |
| await db.query(`INSERT INTO support_messages (id, "senderId", message, timestamp) VALUES ($1, $2, $3, $4)`, [ticketId, senderId, message, Date.now()]); |
| |
| io.emit('support-ticket-updated'); |
| res.status(201).json({ success: true, id: ticketId }); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed to create support ticket' }); |
| } |
| }); |
|
|
| app.get('/api/support/history/:deviceId', async (req, res) => { |
| try { |
| const { deviceId } = req.params; |
| const { rows: history } = await db.query(`SELECT * FROM support_messages WHERE ("senderId" = $1 OR "senderId" LIKE $2) ORDER BY timestamp DESC`, [deviceId, `${deviceId}_%`]); |
| res.json(history); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed' }); |
| } |
| }); |
|
|
| app.get('/api/support/unread/:deviceId', async (req, res) => { |
| try { |
| const { deviceId } = req.params; |
| const { rows } = await db.query(`SELECT COUNT(*) as count FROM support_messages WHERE ("senderId" = $1 OR "senderId" LIKE $2) AND "unreadAdminReply" = 1`, [deviceId, `${deviceId}_%`]); |
| res.json({ count: parseInt(rows[0].count) }); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed' }); |
| } |
| }); |
|
|
| app.patch('/api/support/read/:deviceId', async (req, res) => { |
| try { |
| const { deviceId } = req.params; |
| await db.query(`UPDATE support_messages SET "unreadAdminReply" = 0 WHERE ("senderId" = $1 OR "senderId" LIKE $2)`, [deviceId, `${deviceId}_%`]); |
| res.json({ success: true }); |
| } catch (err) { |
| res.status(500).json({ error: 'Failed' }); |
| } |
| }); |
|
|
| |
| |
| |
| const DEFAULT_ADMIN_HASH = '922b11a4333a2f48c9cd3a55240b26b724d5273d28564e485582b5a375876e46'; |
| const ADMIN_SECRET = process.env.ADMIN_SECRET; |
|
|
| const ADMIN_HASH_EXPECTED = ADMIN_SECRET |
| ? crypto.createHash('sha256').update(ADMIN_SECRET).digest('hex') |
| : DEFAULT_ADMIN_HASH; |
|
|
| const activeAdminTokens = new Set(); |
|
|
| app.post('/api/admin/login', (req, res) => { |
| if (req.body.passwordHash === ADMIN_HASH_EXPECTED) { |
| const sessionToken = uuidv4(); |
| activeAdminTokens.add(sessionToken); |
| res.json({ token: sessionToken }); |
| } else { |
| res.status(401).json({ error: 'Invalid encrypted admin password' }); |
| } |
| }); |
|
|
| const adminAuth = (req, res, next) => { |
| if (activeAdminTokens.has(req.headers['x-admin-key'])) return next(); |
| return res.status(403).json({ error: 'Unauthorized Admin Session.' }); |
| }; |
|
|
| app.get('/api/admin/stats', adminAuth, async (req, res) => { |
| try { |
| const { rows: users } = await db.query(`SELECT count(*) as count FROM users`); |
| const { rows: listings } = await db.query(`SELECT count(*) as count FROM listings`); |
| const { rows: pendingSupport } = await db.query(`SELECT count(*) as count FROM support_messages WHERE "isResolved" = 0`); |
| res.json({ users: parseInt(users[0].count), listings: parseInt(listings[0].count), pendingSupport: parseInt(pendingSupport[0].count) }); |
| } catch (e) { res.status(500).json({ error: 'Failed' }); } |
| }); |
|
|
| app.get('/api/admin/users', adminAuth, async (req, res) => { |
| try { |
| const { rows: users } = await db.query(`SELECT * FROM users ORDER BY id ASC`); |
| res.json(users); |
| } catch(e) { res.status(500).json({ error: 'Failed' }); } |
| }); |
|
|
| app.patch('/api/admin/users/:id/block', adminAuth, async (req, res) => { |
| const { isBlocked } = req.body; |
| try { |
| await db.query(`UPDATE users SET "isBlocked" = $1 WHERE id = $2`, [isBlocked ? 1 : 0, req.params.id]); |
| io.emit('user-blocked-status-changed', { id: req.params.id, isBlocked: isBlocked ? 1 : 0 }); |
| res.json({ success: true }); |
| } catch(e) { res.status(500).json({ error: 'Failed' }); } |
| }); |
|
|
| app.delete('/api/admin/users/:id', adminAuth, async (req, res) => { |
| try { |
| const { rows: users } = await db.query(`SELECT * FROM users WHERE id = $1`, [req.params.id]); |
| const user = users[0]; |
| if (!user) return res.status(404).json({ error: 'Not found' }); |
| |
| |
| try { fs.unlinkSync(path.join(uploadDir, path.basename(user.selfiePath))); } catch(e){} |
| |
| |
| const { rows: userListings } = await db.query(`SELECT id FROM listings WHERE "sellerId" = $1`, [req.params.id]); |
| for (const l of userListings) { |
| const { rows: imgs } = await db.query(`SELECT "imagePath" FROM listing_images WHERE "listingId" = $1`, [l.id]); |
| for (const i of imgs) { |
| try { fs.unlinkSync(path.join(uploadDir, path.basename(i.imagePath))); } catch(e){} |
| } |
| } |
| |
| |
| for (const l of userListings) { |
| await db.query(`DELETE FROM listing_images WHERE "listingId" = $1`, [l.id]); |
| } |
| await db.query(`DELETE FROM listings WHERE "sellerId" = $1`, [req.params.id]); |
| |
| |
| if (user && user.selfiePath) { |
| try { fs.unlinkSync(path.join(process.cwd(), 'server', user.selfiePath)); } catch (e) {} |
| } |
| await db.query(`DELETE FROM users WHERE id = $1`, [req.params.id]); |
|
|
| |
| io.emit('listing-updated', { forceRefresh: true }); |
| |
| res.json({ success: true }); |
| } catch(e) { console.error(e); res.status(500).json({ error: 'Failed to delete' }); } |
| }); |
|
|
| app.get('/api/admin/listings', adminAuth, async (req, res) => { |
| try { |
| const { rows: listings } = await db.query(`SELECT listings.*, users.name as "sellerNameDisplay", users.contact as "sellerContact" FROM listings JOIN users ON users.id = listings."sellerId" ORDER BY timestamp DESC`); |
| const result = await Promise.all(listings.map(async l => { |
| const { rows: imgs } = await db.query(`SELECT "imagePath" FROM listing_images WHERE "listingId" = $1`, [l.id]); |
| return { ...l, images: imgs.map(i => i.imagePath) }; |
| })); |
| res.json(result); |
| } catch(e) { res.status(500).json({ error: 'Failed' }); } |
| }); |
|
|
| app.delete('/api/admin/listings/:id', adminAuth, async (req, res) => { |
| try { |
| const { rows: imgs } = await db.query(`SELECT "imagePath" FROM listing_images WHERE "listingId" = $1`, [req.params.id]); |
| for (const i of imgs) { |
| try { fs.unlinkSync(path.join(uploadDir, path.basename(i.imagePath))); } catch(e){} |
| } |
| await db.query(`DELETE FROM listing_images WHERE "listingId" = $1`, [req.params.id]); |
| await db.query(`DELETE FROM listings WHERE id = $1`, [req.params.id]); |
|
|
| |
| io.emit('listing-updated', { id: req.params.id, status: 'deleted' }); |
|
|
| res.json({ success: true }); |
| } catch(e) { res.status(500).json({ error: 'Failed' }); } |
| }); |
| app.get('/api/admin/support', adminAuth, async (req, res) => { |
| try { |
| const { rows: messages } = await db.query(`SELECT support_messages.*, users.name, users.role, users.contact, users."selfiePath" FROM support_messages LEFT JOIN users ON users.id = support_messages."senderId" ORDER BY support_messages.timestamp DESC`); |
| res.json(messages); |
| } catch(e) { res.status(500).json({ error: 'Failed' }); } |
| }); |
|
|
| app.patch('/api/admin/support/:id/resolve', adminAuth, async (req, res) => { |
| try { |
| await db.query(`UPDATE support_messages SET "isResolved" = 1 WHERE id = $1`, [req.params.id]); |
| res.json({ success: true }); |
| } catch(e) { res.status(500).json({ error: 'Failed' }); } |
| }); |
|
|
| app.patch('/api/admin/support/:id/reply', adminAuth, async (req, res) => { |
| try { |
| const { reply } = req.body; |
| await db.query(`UPDATE support_messages SET "adminReply" = $1, "unreadAdminReply" = 1 WHERE id = $2`, [reply, req.params.id]); |
| io.emit('support-ticket-updated'); |
| res.json({ success: true, reply }); |
| } catch(e) { |
| console.error("Support Reply Error:", e); |
| res.status(500).json({ error: 'Failed' }); |
| } |
| }); |
| |
|
|
| io.on('connection', (socket) => { |
| |
| socket.on('join-network', (deviceId) => { |
| activeUsers.set(deviceId, socket.id); |
| console.log(`User connected: ${deviceId} via socket ${socket.id}`); |
| }); |
|
|
| socket.on('disconnect', () => { |
| for (const [deviceId, sockId] of activeUsers.entries()) { |
| if (sockId === socket.id) { |
| activeUsers.delete(deviceId); |
| console.log(`User disconnected: ${deviceId}`); |
| break; |
| } |
| } |
| }); |
|
|
| const getRawId = (id) => id ? id.split('_')[0] : null; |
|
|
| |
| socket.on('send-message', (data) => { |
| const receiverSocket = activeUsers.get(getRawId(data.receiverId)); |
| if (receiverSocket) { |
| io.to(receiverSocket).emit('receive-message', data); |
| } |
| }); |
|
|
| |
| socket.on('call-user', (data) => { |
| const { userToCall, signalData, from, callerName, callerSelfie } = data; |
| const receiverSocket = activeUsers.get(getRawId(userToCall)); |
| if (receiverSocket) { |
| io.to(receiverSocket).emit('incoming-call', { |
| signal: signalData, |
| from, |
| callerName, |
| callerSelfie |
| }); |
| } else { |
| |
| io.to(socket.id).emit('call-failed', { message: 'User is offline' }); |
| } |
| }); |
|
|
| socket.on('answer-call', (data) => { |
| const callerSocket = activeUsers.get(getRawId(data.to)); |
| if (callerSocket) { |
| io.to(callerSocket).emit('call-accepted', data.signal); |
| } |
| }); |
|
|
| socket.on('end-call', (data) => { |
| const peerSocket = activeUsers.get(getRawId(data.to)); |
| if (peerSocket) { |
| io.to(peerSocket).emit('call-ended'); |
| } |
| }); |
|
|
| socket.on('missed-call', (data) => { |
| const peerSocket = activeUsers.get(getRawId(data.to)); |
| if (peerSocket) io.to(peerSocket).emit('missed-call'); |
| }); |
|
|
| |
| socket.on('request-video', (data) => { |
| const peerSocket = activeUsers.get(getRawId(data.to)); |
| if (peerSocket) io.to(peerSocket).emit('video-requested', { from: data.from }); |
| }); |
|
|
| socket.on('accept-video', (data) => { |
| const peerSocket = activeUsers.get(getRawId(data.to)); |
| if (peerSocket) io.to(peerSocket).emit('video-accepted'); |
| }); |
|
|
| socket.on('reject-video', (data) => { |
| const peerSocket = activeUsers.get(getRawId(data.to)); |
| if (peerSocket) io.to(peerSocket).emit('video-rejected'); |
| }); |
| }); |
|
|
| |
| |
| const spaDistDir = path.resolve('./dist'); |
| const spaIndexHtml = path.join(spaDistDir, 'index.html'); |
| if (fs.existsSync(spaIndexHtml)) { |
| app.get('/{*splat}', (req, res) => { |
| res.sendFile(spaIndexHtml); |
| }); |
| } |
|
|
| httpServer.listen(PORT, '0.0.0.0', async () => { |
| await initDB(); |
| console.log(`Backend API & WebSocket Server running at http://0.0.0.0:${PORT}`); |
| await loadModel(); |
| }); |
|
|