278 lines
8.2 KiB
JavaScript
278 lines
8.2 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const base64url = require('base64url');
|
|
const multer = require('multer');
|
|
const { log } = require('console');
|
|
const bodyParser = require('body-parser');
|
|
const { convertSvgToPng } = require('./utils/imageConverter');
|
|
const websocket = require('./websocket');
|
|
const translations = require('./translations');
|
|
|
|
const app = express();
|
|
const upload = multer({ storage: multer.memoryStorage() });
|
|
|
|
// Configure EJS
|
|
app.set('view engine', 'ejs');
|
|
app.set('views', path.join(__dirname, 'views'));
|
|
|
|
app.use(express.urlencoded({ extended: true }));
|
|
app.use(bodyParser.json({ limit: "50mb" }));
|
|
|
|
// Serve static files
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// Initialize WebSocket server
|
|
const server = app.listen(80, () => {
|
|
console.log('Server running on port 80');
|
|
});
|
|
websocket.initialize(server);
|
|
|
|
|
|
|
|
|
|
// Static images mapping
|
|
const staticImages = {
|
|
mulberry: 'am_mulberry_logo_wide_color.png' ,
|
|
home: 'payment.png',
|
|
scan: 'Scanner.png',
|
|
printer: 'Check.png',
|
|
settings: 'Settings.png',
|
|
pinpad: 'pinpad.png',
|
|
cashier: 'cashier.png',
|
|
menu: 'Menu.png',
|
|
};
|
|
|
|
// Topics list
|
|
const topics = [
|
|
|
|
{ deviceName: 'M50F_Second', deviceID: '01534090202502210057' },
|
|
{ deviceName: 'M50F_Igor', deviceID: '01534090202502210142' },
|
|
{ deviceName: 'M60_Igor', deviceID: '01620013202312220735' },
|
|
{ deviceName: 'M70_Igor', deviceID: '01723060202412010280' },
|
|
{ deviceName: 'M30_Igor', deviceID: '01364100202503060196' },
|
|
|
|
{ deviceName: 'M80F_Ahmed', deviceID: '01804042025072100015' },
|
|
{ deviceName: 'M80K_Ahmed', deviceID: '78040132025082600001' },
|
|
{ deviceName: 'M50F_Ahmed', deviceID: '01534090202502210065' },
|
|
{ deviceName: 'M50F_Ahmed', deviceID: '01534090202502210065' },
|
|
{ deviceName: 'M60_Ahmed', deviceID: '01620013202312221500' },
|
|
{ deviceName: 'M30_Ahmed', deviceID: '01364100202503060115' },
|
|
{ deviceName: 'M20_Ahmed', deviceID: '01220110202305150514' },
|
|
{ deviceName: 'M70_Ahmed', deviceID: '01723060202412010160' },
|
|
|
|
{ deviceName: 'M60F-NOSN', deviceID: 'm60f_pos_no_sn' }
|
|
];
|
|
|
|
// Sample configuration
|
|
const defaultConfig = {
|
|
type: "configuration",
|
|
navItems: [
|
|
{
|
|
navPage: "NAV_HOME",
|
|
navName: "Оплата",
|
|
icon: { type: "resource", value: "home" },
|
|
position: 0,
|
|
enabled: true
|
|
},
|
|
{
|
|
navPage: "NAV_SCAN",
|
|
navName: "Сканнер",
|
|
icon: { type: "resource", value: "scan" },
|
|
position: 1,
|
|
enabled: true
|
|
},
|
|
{
|
|
navPage: "NAV_PRINTER",
|
|
navName: "Принтер",
|
|
icon: { type: "resource", value: "printer" },
|
|
position: 2,
|
|
enabled: true
|
|
},
|
|
{
|
|
navPage: "NAV_SETTINGS",
|
|
navName: "Настройки",
|
|
icon: { type: "resource", value: "settings" },
|
|
position: 3,
|
|
enabled: true
|
|
}
|
|
],
|
|
mainLogo: {
|
|
type: "base64",
|
|
value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABlUAAAE "
|
|
},
|
|
footerLogo: {
|
|
type: "base64",
|
|
value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaQAAAWY"
|
|
},
|
|
printLogo: {
|
|
type: "base64",
|
|
value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaQAAAWY"
|
|
},
|
|
printURL: "www.mulberrypos.ru",
|
|
footerLogoVisibility: "true",
|
|
bankMode: false,
|
|
footerGreetingText: "Привет Игорь! ))",
|
|
footerText: "Mulberry, OOO Demo ©2025",
|
|
primaryColor: "#002233",
|
|
secondaryColor: "#67777eff",
|
|
};
|
|
|
|
// Routes
|
|
app.get('/', (req, res) => {
|
|
res.render('index', {
|
|
staticImages: Object.keys(staticImages),
|
|
topics,
|
|
defaultConfig,
|
|
connectedDevices: websocket.getConnectedDevices()
|
|
});
|
|
});
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
websocketConnections: websocket.connectedDevices.size
|
|
});
|
|
});
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', gracefulShutdown);
|
|
process.on('SIGINT', gracefulShutdown);
|
|
|
|
function gracefulShutdown() {
|
|
console.log('Shutting down gracefully...');
|
|
websocket.cleanup();
|
|
server.close(() => {
|
|
console.log('Server closed');
|
|
process.exit(0);
|
|
});
|
|
}
|
|
|
|
app.post('/sendToDevice', (req, res) => {
|
|
const { config, topic } = req.body;
|
|
console.log(`Sending config to device ${topic}`);
|
|
const success = websocket.sendToDevice(topic, config);
|
|
res.status(success ? 200 : 404).send(success ? 'Message sent' : 'Device not connected');
|
|
});
|
|
|
|
// Payment endpoints
|
|
// Payment endpoint
|
|
app.get('/pay', (req, res) => {
|
|
try {
|
|
const paymentData = JSON.parse(base64url.decode(req.query.data));
|
|
|
|
if (!paymentData.sn || !paymentData.amount) {
|
|
return res.redirect(`/error?message=${encodeURIComponent('Invalid payment data')}`);
|
|
}
|
|
console.log(paymentData);
|
|
|
|
// Convert the amount from cents to dollars
|
|
const amountInDollars = parseFloat(paymentData.amount) / 100;
|
|
|
|
// Format the amount to two decimal places
|
|
const formattedAmount = amountInDollars.toFixed(2);
|
|
|
|
const language = paymentData.lang;
|
|
// Get translations for the selected language
|
|
const t = translations[language] || translations.en;
|
|
|
|
// const event = new Date(paymentData.time);
|
|
|
|
// const russianDate = event.toLocaleString("ru-RU", { timeZone: "Etc/GMT-3" })
|
|
|
|
|
|
const event = new Date(paymentData.time);
|
|
const russianDate = event.toLocaleString("ru-RU", {
|
|
timeZone: "Europe/Moscow",
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false
|
|
}).replace(',', '');
|
|
|
|
// Format data for the receipt
|
|
const receiptData = {
|
|
deviceId: paymentData.sn,
|
|
t:t,
|
|
appLogo: paymentData.appLogo,
|
|
appColor: paymentData.appColor,
|
|
currencyS: paymentData.currencyS,
|
|
paymentMethod: "QR Code",
|
|
amount: formattedAmount,
|
|
timestamp: russianDate
|
|
,
|
|
transactionId: `TXN-${Math.random().toString(36).substr(2, 9).toUpperCase()}`
|
|
};
|
|
|
|
// Render EJS template with data
|
|
res.render('receipt', {
|
|
title: 'DatexPay Virtual Bank - Receipt',
|
|
...receiptData
|
|
});
|
|
|
|
} catch (error) {
|
|
res.redirect(`/error?message=${encodeURIComponent(error.message)}`);
|
|
}
|
|
});
|
|
|
|
app.post('/pay-command', (req, res) => {
|
|
console.log("pay command");
|
|
|
|
const id = req.query.id;
|
|
websocket.sendToDevice(id, {
|
|
type: 'payment',
|
|
action: 'PRINT_RECEIPT',
|
|
status : "success" });
|
|
res.send({ message: 'Receipt print command sent!' });
|
|
});
|
|
|
|
app.post('/cancel-command', (req, res) => {
|
|
|
|
|
|
const id = req.query.id;
|
|
console.log("cancel command id :" + id);
|
|
websocket.sendToDevice(id, { type: 'payment', action: 'REJECT_RECEIPT',
|
|
status : "failed" });
|
|
res.send({ message: 'Receipt rejection command sent!' });
|
|
});
|
|
|
|
// Error page
|
|
app.get('/error', (req, res) => {
|
|
res.render('error', {
|
|
title: 'Payment Error',
|
|
message: req.query.message || 'An unknown error occurred'
|
|
});
|
|
});
|
|
|
|
// File upload
|
|
app.post('/upload', upload.single('image'), async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No file uploaded' });
|
|
}
|
|
|
|
let base64Data;
|
|
const isSvg = req.body.isSvg === 'true';
|
|
|
|
if (isSvg) {
|
|
const svgContent = req.file.buffer.toString('utf-8');
|
|
const pngBuffer = await convertSvgToPng(svgContent, 'LARGE');
|
|
base64Data = pngBuffer.toString('base64');
|
|
} else {
|
|
base64Data = req.file.buffer.toString('base64');
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
base64: `data:image/${isSvg ? 'png' : req.file.mimetype.split('/')[1]};base64,${base64Data}`,
|
|
fileName: req.file.originalname,
|
|
inputName: req.body.inputName
|
|
});
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
res.status(500).json({ error: 'Image processing failed' });
|
|
}
|
|
}); |