Umstellung auf sqlite
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,9 @@
|
|||||||
.env
|
.env
|
||||||
data/codes.json
|
data/codes.json
|
||||||
data/pending-orders.json
|
data/pending-orders.json
|
||||||
|
data/*.db
|
||||||
|
data/*.db-wal
|
||||||
|
data/*.db-shm
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
358
db.js
Normal file
358
db.js
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
/**
|
||||||
|
* SQLite database layer for UniFi Portal (replaces JSON files).
|
||||||
|
* DB file: data/portal.db
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const DATA_DIR = path.join(__dirname, 'data');
|
||||||
|
const DB_PATH = path.join(DATA_DIR, 'portal.db');
|
||||||
|
|
||||||
|
let db = null;
|
||||||
|
|
||||||
|
function getDb() {
|
||||||
|
if (!db) {
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
if (!fs.existsSync(DATA_DIR)) {
|
||||||
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
db = new Database(DB_PATH);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
initSchema(db);
|
||||||
|
migrateFromJson(db);
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSchema(database) {
|
||||||
|
database.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS codes (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
createdAt TEXT NOT NULL,
|
||||||
|
expiresAt TEXT,
|
||||||
|
useCount INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS packages (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
durationMinutes INTEGER NOT NULL,
|
||||||
|
price REAL NOT NULL,
|
||||||
|
currency TEXT NOT NULL,
|
||||||
|
active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
sortOrder INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pending_orders (
|
||||||
|
orderId TEXT PRIMARY KEY,
|
||||||
|
guestMac TEXT NOT NULL,
|
||||||
|
packageId TEXT NOT NULL,
|
||||||
|
createdAt TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateFromJson(database) {
|
||||||
|
const codesPath = path.join(DATA_DIR, 'codes.json');
|
||||||
|
const packagesPath = path.join(DATA_DIR, 'packages.json');
|
||||||
|
const ordersPath = path.join(DATA_DIR, 'pending-orders.json');
|
||||||
|
|
||||||
|
const hasCodes = database.prepare('SELECT 1 FROM codes LIMIT 1').get();
|
||||||
|
if (!hasCodes && fs.existsSync(codesPath)) {
|
||||||
|
try {
|
||||||
|
const codes = JSON.parse(fs.readFileSync(codesPath, 'utf8'));
|
||||||
|
const insert = database.prepare(
|
||||||
|
'INSERT OR IGNORE INTO codes (code, createdAt, expiresAt, useCount) VALUES (?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
for (const c of codes) {
|
||||||
|
insert.run(
|
||||||
|
c.code,
|
||||||
|
c.createdAt || new Date().toISOString(),
|
||||||
|
c.expiresAt || null,
|
||||||
|
c.useCount ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`Migrated ${codes.length} codes from JSON to SQLite`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Migration codes.json:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPackages = database.prepare('SELECT 1 FROM packages LIMIT 1').get();
|
||||||
|
if (!hasPackages && fs.existsSync(packagesPath)) {
|
||||||
|
try {
|
||||||
|
const packages = JSON.parse(fs.readFileSync(packagesPath, 'utf8'));
|
||||||
|
const insert = database.prepare(
|
||||||
|
'INSERT OR IGNORE INTO packages (id, name, durationMinutes, price, currency, active, sortOrder) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
for (const p of packages) {
|
||||||
|
insert.run(
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.durationMinutes ?? 0,
|
||||||
|
p.price ?? 0,
|
||||||
|
p.currency || 'EUR',
|
||||||
|
p.active !== false ? 1 : 0,
|
||||||
|
p.sortOrder ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`Migrated ${packages.length} packages from JSON to SQLite`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Migration packages.json:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasOrders = database.prepare('SELECT 1 FROM pending_orders LIMIT 1').get();
|
||||||
|
if (!hasOrders && fs.existsSync(ordersPath)) {
|
||||||
|
try {
|
||||||
|
const orders = JSON.parse(fs.readFileSync(ordersPath, 'utf8'));
|
||||||
|
const insert = database.prepare(
|
||||||
|
'INSERT OR IGNORE INTO pending_orders (orderId, guestMac, packageId, createdAt) VALUES (?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
for (const [orderId, o] of Object.entries(orders)) {
|
||||||
|
insert.run(orderId, o.guestMac, o.packageId, o.createdAt || new Date().toISOString());
|
||||||
|
}
|
||||||
|
const count = Object.keys(orders).length;
|
||||||
|
if (count) console.log(`Migrated ${count} pending orders from JSON to SQLite`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Migration pending-orders.json:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Codes ---
|
||||||
|
|
||||||
|
function loadCodes() {
|
||||||
|
const rows = getDb().prepare('SELECT code, createdAt, expiresAt, useCount FROM codes').all();
|
||||||
|
return rows.map((r) => ({
|
||||||
|
code: r.code,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
expiresAt: r.expiresAt,
|
||||||
|
useCount: r.useCount ?? 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCodes(codes) {
|
||||||
|
const database = getDb();
|
||||||
|
database.prepare('DELETE FROM codes').run();
|
||||||
|
const insert = database.prepare(
|
||||||
|
'INSERT INTO codes (code, createdAt, expiresAt, useCount) VALUES (?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
for (const c of codes) {
|
||||||
|
insert.run(c.code, c.createdAt, c.expiresAt ?? null, c.useCount ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAndValidateCode(codeInput) {
|
||||||
|
const normalized = String(codeInput || '').trim().toUpperCase();
|
||||||
|
if (!normalized) return null;
|
||||||
|
const row = getDb()
|
||||||
|
.prepare('SELECT code, createdAt, expiresAt, useCount FROM codes WHERE code = ?')
|
||||||
|
.get(normalized);
|
||||||
|
if (!row) return null;
|
||||||
|
if (row.expiresAt && new Date(row.expiresAt) < new Date()) return null;
|
||||||
|
return {
|
||||||
|
code: row.code,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
expiresAt: row.expiresAt,
|
||||||
|
useCount: row.useCount ?? 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementCodeUseCount(codeInput) {
|
||||||
|
const normalized = String(codeInput || '').trim().toUpperCase();
|
||||||
|
getDb().prepare('UPDATE codes SET useCount = useCount + 1 WHERE code = ?').run(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CODE_CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||||
|
|
||||||
|
function generateCode() {
|
||||||
|
let code = '';
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
code += CODE_CHARS.charAt(Math.floor(Math.random() * CODE_CHARS.length));
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCode(expiresInMinutes) {
|
||||||
|
const code = generateCode();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const expiresAt = expiresInMinutes
|
||||||
|
? new Date(Date.now() + expiresInMinutes * 60 * 1000).toISOString()
|
||||||
|
: null;
|
||||||
|
getDb()
|
||||||
|
.prepare('INSERT INTO codes (code, createdAt, expiresAt, useCount) VALUES (?, ?, ?, 0)')
|
||||||
|
.run(code, now, expiresAt);
|
||||||
|
return { code, createdAt: now, expiresAt, useCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCode(codeInput) {
|
||||||
|
const normalized = String(codeInput || '').trim().toUpperCase();
|
||||||
|
const result = getDb().prepare('DELETE FROM codes WHERE code = ?').run(normalized);
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Packages ---
|
||||||
|
|
||||||
|
function loadPackages() {
|
||||||
|
const rows = getDb()
|
||||||
|
.prepare(
|
||||||
|
'SELECT id, name, durationMinutes, price, currency, active, sortOrder FROM packages'
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
durationMinutes: r.durationMinutes,
|
||||||
|
price: r.price,
|
||||||
|
currency: r.currency,
|
||||||
|
active: Boolean(r.active),
|
||||||
|
sortOrder: r.sortOrder ?? 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePackages(packages) {
|
||||||
|
const database = getDb();
|
||||||
|
database.prepare('DELETE FROM packages').run();
|
||||||
|
const insert = database.prepare(
|
||||||
|
'INSERT INTO packages (id, name, durationMinutes, price, currency, active, sortOrder) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
for (const p of packages) {
|
||||||
|
insert.run(
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.durationMinutes,
|
||||||
|
p.price,
|
||||||
|
p.currency || 'EUR',
|
||||||
|
p.active !== false ? 1 : 0,
|
||||||
|
p.sortOrder ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPackageById(id) {
|
||||||
|
const row = getDb()
|
||||||
|
.prepare(
|
||||||
|
'SELECT id, name, durationMinutes, price, currency, active, sortOrder FROM packages WHERE id = ?'
|
||||||
|
)
|
||||||
|
.get(id);
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
durationMinutes: row.durationMinutes,
|
||||||
|
price: row.price,
|
||||||
|
currency: row.currency,
|
||||||
|
active: Boolean(row.active),
|
||||||
|
sortOrder: row.sortOrder ?? 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPackage({ name, durationMinutes, price, currency, active = true }) {
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const database = getDb();
|
||||||
|
const maxOrder = database.prepare('SELECT COALESCE(MAX(sortOrder), 0) AS m FROM packages').get();
|
||||||
|
const sortOrder = (maxOrder?.m ?? 0) + 1;
|
||||||
|
database
|
||||||
|
.prepare(
|
||||||
|
'INSERT INTO packages (id, name, durationMinutes, price, currency, active, sortOrder) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
)
|
||||||
|
.run(
|
||||||
|
id,
|
||||||
|
String(name || '').trim(),
|
||||||
|
parseInt(durationMinutes, 10) || 60,
|
||||||
|
parseFloat(price) || 0,
|
||||||
|
String(currency || 'EUR').toUpperCase().slice(0, 3),
|
||||||
|
active ? 1 : 0,
|
||||||
|
sortOrder
|
||||||
|
);
|
||||||
|
return findPackageById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePackage(id, updates) {
|
||||||
|
const current = findPackageById(id);
|
||||||
|
if (!current) return null;
|
||||||
|
const next = {
|
||||||
|
...current,
|
||||||
|
...(updates.name !== undefined && { name: String(updates.name).trim() }),
|
||||||
|
...(updates.durationMinutes !== undefined && {
|
||||||
|
durationMinutes: parseInt(updates.durationMinutes, 10) || current.durationMinutes
|
||||||
|
}),
|
||||||
|
...(updates.price !== undefined && { price: parseFloat(updates.price) ?? current.price }),
|
||||||
|
...(updates.currency !== undefined && {
|
||||||
|
currency: String(updates.currency).toUpperCase().slice(0, 3)
|
||||||
|
}),
|
||||||
|
...(updates.active !== undefined && { active: Boolean(updates.active) }),
|
||||||
|
...(updates.sortOrder !== undefined && { sortOrder: parseInt(updates.sortOrder, 10) })
|
||||||
|
};
|
||||||
|
getDb()
|
||||||
|
.prepare(
|
||||||
|
'UPDATE packages SET name = ?, durationMinutes = ?, price = ?, currency = ?, active = ?, sortOrder = ? WHERE id = ?'
|
||||||
|
)
|
||||||
|
.run(
|
||||||
|
next.name,
|
||||||
|
next.durationMinutes,
|
||||||
|
next.price,
|
||||||
|
next.currency,
|
||||||
|
next.active ? 1 : 0,
|
||||||
|
next.sortOrder,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return findPackageById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePackage(id) {
|
||||||
|
const result = getDb().prepare('DELETE FROM packages WHERE id = ?').run(id);
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pending orders ---
|
||||||
|
|
||||||
|
const PENDING_ORDER_TTL_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
|
function addPendingOrder(orderId, { guestMac, packageId }) {
|
||||||
|
getDb()
|
||||||
|
.prepare(
|
||||||
|
'INSERT OR REPLACE INTO pending_orders (orderId, guestMac, packageId, createdAt) VALUES (?, ?, ?, ?)'
|
||||||
|
)
|
||||||
|
.run(orderId, guestMac, packageId, new Date().toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAndRemovePendingOrder(orderId) {
|
||||||
|
const row = getDb()
|
||||||
|
.prepare('SELECT orderId, guestMac, packageId, createdAt FROM pending_orders WHERE orderId = ?')
|
||||||
|
.get(orderId);
|
||||||
|
if (!row) return null;
|
||||||
|
const created = new Date(row.createdAt).getTime();
|
||||||
|
if (Date.now() - created > PENDING_ORDER_TTL_MS) {
|
||||||
|
getDb().prepare('DELETE FROM pending_orders WHERE orderId = ?').run(orderId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
getDb().prepare('DELETE FROM pending_orders WHERE orderId = ?').run(orderId);
|
||||||
|
return {
|
||||||
|
guestMac: row.guestMac,
|
||||||
|
packageId: row.packageId,
|
||||||
|
createdAt: row.createdAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getDb,
|
||||||
|
loadCodes,
|
||||||
|
saveCodes,
|
||||||
|
findAndValidateCode,
|
||||||
|
incrementCodeUseCount,
|
||||||
|
addCode,
|
||||||
|
deleteCode,
|
||||||
|
loadPackages,
|
||||||
|
savePackages,
|
||||||
|
findPackageById,
|
||||||
|
addPackage,
|
||||||
|
updatePackage,
|
||||||
|
deletePackage,
|
||||||
|
addPendingOrder,
|
||||||
|
getAndRemovePendingOrder,
|
||||||
|
PENDING_ORDER_TTL_MS
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"better-sqlite3": "^12.6.2",
|
||||||
"dotenv": "^17.2.4",
|
"dotenv": "^17.2.4",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3"
|
"express-session": "^1.17.3"
|
||||||
|
|||||||
229
server.js
229
server.js
@@ -3,211 +3,16 @@ const express = require('express');
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
|
|
||||||
|
const db = require('./db');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 80;
|
const PORT = 80;
|
||||||
|
|
||||||
// Admin Configuration
|
// Admin Configuration
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'change-this-admin-password';
|
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'change-this-admin-password';
|
||||||
|
|
||||||
// Code storage (data/codes.json)
|
|
||||||
const DATA_DIR = path.join(__dirname, 'data');
|
|
||||||
const CODES_FILE = path.join(DATA_DIR, 'codes.json');
|
|
||||||
// 8-char alphanumeric, excludes confusing chars (0, O, 1, I, l)
|
|
||||||
const CODE_CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
|
|
||||||
|
|
||||||
function ensureDataDir() {
|
|
||||||
if (!fs.existsSync(DATA_DIR)) {
|
|
||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCodes() {
|
|
||||||
ensureDataDir();
|
|
||||||
if (!fs.existsSync(CODES_FILE)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const data = fs.readFileSync(CODES_FILE, 'utf8');
|
|
||||||
return JSON.parse(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading codes:', err);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCodes(codes) {
|
|
||||||
ensureDataDir();
|
|
||||||
fs.writeFileSync(CODES_FILE, JSON.stringify(codes, null, 2), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateCode() {
|
|
||||||
let code = '';
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
code += CODE_CHARS.charAt(Math.floor(Math.random() * CODE_CHARS.length));
|
|
||||||
}
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAndValidateCode(codeInput) {
|
|
||||||
const codes = loadCodes();
|
|
||||||
const normalized = String(codeInput || '').trim().toUpperCase();
|
|
||||||
if (!normalized) return null;
|
|
||||||
const entry = codes.find((c) => c.code === normalized);
|
|
||||||
if (!entry) return null;
|
|
||||||
if (entry.expiresAt && new Date(entry.expiresAt) < new Date()) {
|
|
||||||
return null; // expired
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementCodeUseCount(codeInput) {
|
|
||||||
const codes = loadCodes();
|
|
||||||
const normalized = String(codeInput || '').trim().toUpperCase();
|
|
||||||
const idx = codes.findIndex((c) => c.code === normalized);
|
|
||||||
if (idx === -1) return;
|
|
||||||
codes[idx].useCount = (codes[idx].useCount || 0) + 1;
|
|
||||||
saveCodes(codes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCode(expiresInMinutes) {
|
|
||||||
const code = generateCode();
|
|
||||||
const now = new Date();
|
|
||||||
const expiresAt = expiresInMinutes
|
|
||||||
? new Date(now.getTime() + expiresInMinutes * 60 * 1000)
|
|
||||||
: null;
|
|
||||||
const entry = {
|
|
||||||
code,
|
|
||||||
createdAt: now.toISOString(),
|
|
||||||
expiresAt: expiresAt ? expiresAt.toISOString() : null,
|
|
||||||
useCount: 0
|
|
||||||
};
|
|
||||||
const codes = loadCodes();
|
|
||||||
codes.push(entry);
|
|
||||||
saveCodes(codes);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteCode(codeInput) {
|
|
||||||
const codes = loadCodes();
|
|
||||||
const normalized = String(codeInput || '').trim().toUpperCase();
|
|
||||||
const filtered = codes.filter((c) => c.code !== normalized);
|
|
||||||
if (filtered.length === codes.length) return false;
|
|
||||||
saveCodes(filtered);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packages storage (data/packages.json)
|
|
||||||
const PACKAGES_FILE = path.join(DATA_DIR, 'packages.json');
|
|
||||||
const PENDING_ORDERS_FILE = path.join(DATA_DIR, 'pending-orders.json');
|
|
||||||
const PENDING_ORDER_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
||||||
|
|
||||||
function loadPackages() {
|
|
||||||
ensureDataDir();
|
|
||||||
if (!fs.existsSync(PACKAGES_FILE)) return [];
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(PACKAGES_FILE, 'utf8'));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading packages:', err);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePackages(packages) {
|
|
||||||
ensureDataDir();
|
|
||||||
fs.writeFileSync(PACKAGES_FILE, JSON.stringify(packages, null, 2), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPackageById(id) {
|
|
||||||
return loadPackages().find((p) => p.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPackage({ name, durationMinutes, price, currency, active = true }) {
|
|
||||||
const packages = loadPackages();
|
|
||||||
const maxOrder = packages.reduce((m, p) => Math.max(m, p.sortOrder || 0), 0);
|
|
||||||
const entry = {
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
name: String(name || '').trim(),
|
|
||||||
durationMinutes: parseInt(durationMinutes, 10) || 60,
|
|
||||||
price: parseFloat(price) || 0,
|
|
||||||
currency: String(currency || 'EUR').toUpperCase().slice(0, 3),
|
|
||||||
active: Boolean(active),
|
|
||||||
sortOrder: maxOrder + 1
|
|
||||||
};
|
|
||||||
packages.push(entry);
|
|
||||||
savePackages(packages);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePackage(id, updates) {
|
|
||||||
const packages = loadPackages();
|
|
||||||
const idx = packages.findIndex((p) => p.id === id);
|
|
||||||
if (idx === -1) return null;
|
|
||||||
packages[idx] = {
|
|
||||||
...packages[idx],
|
|
||||||
...(updates.name !== undefined && { name: String(updates.name).trim() }),
|
|
||||||
...(updates.durationMinutes !== undefined && { durationMinutes: parseInt(updates.durationMinutes, 10) || packages[idx].durationMinutes }),
|
|
||||||
...(updates.price !== undefined && { price: parseFloat(updates.price) || packages[idx].price }),
|
|
||||||
...(updates.currency !== undefined && { currency: String(updates.currency).toUpperCase().slice(0, 3) }),
|
|
||||||
...(updates.active !== undefined && { active: Boolean(updates.active) }),
|
|
||||||
...(updates.sortOrder !== undefined && { sortOrder: parseInt(updates.sortOrder, 10) })
|
|
||||||
};
|
|
||||||
savePackages(packages);
|
|
||||||
return packages[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePackage(id) {
|
|
||||||
const packages = loadPackages();
|
|
||||||
const filtered = packages.filter((p) => p.id !== id);
|
|
||||||
if (filtered.length === packages.length) return false;
|
|
||||||
savePackages(filtered);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPendingOrders() {
|
|
||||||
ensureDataDir();
|
|
||||||
if (!fs.existsSync(PENDING_ORDERS_FILE)) return {};
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(PENDING_ORDERS_FILE, 'utf8'));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading pending orders:', err);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePendingOrders(obj) {
|
|
||||||
ensureDataDir();
|
|
||||||
fs.writeFileSync(PENDING_ORDERS_FILE, JSON.stringify(obj, null, 2), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPendingOrder(orderId, { guestMac, packageId }) {
|
|
||||||
const orders = loadPendingOrders();
|
|
||||||
orders[orderId] = {
|
|
||||||
guestMac,
|
|
||||||
packageId,
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
savePendingOrders(orders);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAndRemovePendingOrder(orderId) {
|
|
||||||
const orders = loadPendingOrders();
|
|
||||||
const entry = orders[orderId];
|
|
||||||
if (!entry) return null;
|
|
||||||
const created = new Date(entry.createdAt).getTime();
|
|
||||||
if (Date.now() - created > PENDING_ORDER_TTL_MS) {
|
|
||||||
delete orders[orderId];
|
|
||||||
savePendingOrders(orders);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
delete orders[orderId];
|
|
||||||
savePendingOrders(orders);
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telegram Configuration
|
// Telegram Configuration
|
||||||
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
||||||
@@ -328,7 +133,7 @@ app.post('/authorize', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeEntry = findAndValidateCode(code);
|
const codeEntry = db.findAndValidateCode(code);
|
||||||
if (!codeEntry) {
|
if (!codeEntry) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -350,7 +155,7 @@ app.post('/authorize', async (req, res) => {
|
|||||||
|
|
||||||
console.log('Authorization successful!', authorizeResponse.data);
|
console.log('Authorization successful!', authorizeResponse.data);
|
||||||
|
|
||||||
incrementCodeUseCount(code);
|
db.incrementCodeUseCount(code);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -394,7 +199,7 @@ app.get('/admin/check', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/admin/codes', requireAdmin, (req, res) => {
|
app.get('/admin/codes', requireAdmin, (req, res) => {
|
||||||
const codes = loadCodes();
|
const codes = db.loadCodes();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const filtered = codes.map((c) => ({
|
const filtered = codes.map((c) => ({
|
||||||
...c,
|
...c,
|
||||||
@@ -405,12 +210,12 @@ app.get('/admin/codes', requireAdmin, (req, res) => {
|
|||||||
|
|
||||||
app.post('/admin/codes', requireAdmin, (req, res) => {
|
app.post('/admin/codes', requireAdmin, (req, res) => {
|
||||||
const { expiresInMinutes } = req.body || {};
|
const { expiresInMinutes } = req.body || {};
|
||||||
const entry = addCode(expiresInMinutes != null ? Number(expiresInMinutes) : null);
|
const entry = db.addCode(expiresInMinutes != null ? Number(expiresInMinutes) : null);
|
||||||
res.json({ success: true, code: entry.code, entry });
|
res.json({ success: true, code: entry.code, entry });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/admin/codes/:code', requireAdmin, (req, res) => {
|
app.delete('/admin/codes/:code', requireAdmin, (req, res) => {
|
||||||
const deleted = deleteCode(req.params.code);
|
const deleted = db.deleteCode(req.params.code);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
return res.json({ success: true });
|
return res.json({ success: true });
|
||||||
}
|
}
|
||||||
@@ -419,7 +224,7 @@ app.delete('/admin/codes/:code', requireAdmin, (req, res) => {
|
|||||||
|
|
||||||
// Public packages (active only)
|
// Public packages (active only)
|
||||||
app.get('/api/packages', (req, res) => {
|
app.get('/api/packages', (req, res) => {
|
||||||
const packages = loadPackages()
|
const packages = db.loadPackages()
|
||||||
.filter((p) => p.active)
|
.filter((p) => p.active)
|
||||||
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
|
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
|
||||||
.map((p) => ({
|
.map((p) => ({
|
||||||
@@ -434,7 +239,7 @@ app.get('/api/packages', (req, res) => {
|
|||||||
|
|
||||||
// Admin packages CRUD
|
// Admin packages CRUD
|
||||||
app.get('/admin/packages', requireAdmin, (req, res) => {
|
app.get('/admin/packages', requireAdmin, (req, res) => {
|
||||||
const packages = loadPackages().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
const packages = db.loadPackages().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
||||||
res.json({ packages });
|
res.json({ packages });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -443,18 +248,18 @@ app.post('/admin/packages', requireAdmin, (req, res) => {
|
|||||||
if (!name || !durationMinutes || price == null) {
|
if (!name || !durationMinutes || price == null) {
|
||||||
return res.status(400).json({ success: false, error: 'Missing required fields: name, durationMinutes, price' });
|
return res.status(400).json({ success: false, error: 'Missing required fields: name, durationMinutes, price' });
|
||||||
}
|
}
|
||||||
const entry = addPackage({ name, durationMinutes, price, currency: currency || 'EUR', active });
|
const entry = db.addPackage({ name, durationMinutes, price, currency: currency || 'EUR', active });
|
||||||
res.json({ success: true, package: entry });
|
res.json({ success: true, package: entry });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put('/admin/packages/:id', requireAdmin, (req, res) => {
|
app.put('/admin/packages/:id', requireAdmin, (req, res) => {
|
||||||
const pkg = updatePackage(req.params.id, req.body || {});
|
const pkg = db.updatePackage(req.params.id, req.body || {});
|
||||||
if (!pkg) return res.status(404).json({ success: false, error: 'Package not found' });
|
if (!pkg) return res.status(404).json({ success: false, error: 'Package not found' });
|
||||||
res.json({ success: true, package: pkg });
|
res.json({ success: true, package: pkg });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/admin/packages/:id', requireAdmin, (req, res) => {
|
app.delete('/admin/packages/:id', requireAdmin, (req, res) => {
|
||||||
const deleted = deletePackage(req.params.id);
|
const deleted = db.deletePackage(req.params.id);
|
||||||
if (!deleted) return res.status(404).json({ success: false, error: 'Package not found' });
|
if (!deleted) return res.status(404).json({ success: false, error: 'Package not found' });
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
@@ -466,7 +271,7 @@ app.post('/api/paypal/create-order', async (req, res) => {
|
|||||||
return res.status(400).json({ success: false, error: 'Session expired. Please reconnect to WiFi.' });
|
return res.status(400).json({ success: false, error: 'Session expired. Please reconnect to WiFi.' });
|
||||||
}
|
}
|
||||||
const { packageId } = req.body || {};
|
const { packageId } = req.body || {};
|
||||||
const pkg = findPackageById(packageId);
|
const pkg = db.findPackageById(packageId);
|
||||||
if (!pkg || !pkg.active) {
|
if (!pkg || !pkg.active) {
|
||||||
return res.status(400).json({ success: false, error: 'Invalid package' });
|
return res.status(400).json({ success: false, error: 'Invalid package' });
|
||||||
}
|
}
|
||||||
@@ -497,7 +302,7 @@ app.post('/api/paypal/create-order', async (req, res) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const orderId = resPayPal.data.id;
|
const orderId = resPayPal.data.id;
|
||||||
addPendingOrder(orderId, {
|
db.addPendingOrder(orderId, {
|
||||||
guestMac: req.session.guestMac,
|
guestMac: req.session.guestMac,
|
||||||
packageId: pkg.id
|
packageId: pkg.id
|
||||||
});
|
});
|
||||||
@@ -523,12 +328,12 @@ app.post('/api/paypal/capture-order', async (req, res) => {
|
|||||||
return res.status(400).json({ success: false, error: 'Missing orderId' });
|
return res.status(400).json({ success: false, error: 'Missing orderId' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = getAndRemovePendingOrder(orderId);
|
const pending = db.getAndRemovePendingOrder(orderId);
|
||||||
if (!pending || pending.guestMac !== guestMac) {
|
if (!pending || pending.guestMac !== guestMac) {
|
||||||
return res.status(400).json({ success: false, error: 'Invalid or expired order' });
|
return res.status(400).json({ success: false, error: 'Invalid or expired order' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkg = findPackageById(pending.packageId);
|
const pkg = db.findPackageById(pending.packageId);
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
return res.status(400).json({ success: false, error: 'Package no longer available' });
|
return res.status(400).json({ success: false, error: 'Package no longer available' });
|
||||||
}
|
}
|
||||||
@@ -550,7 +355,7 @@ app.post('/api/paypal/capture-order', async (req, res) => {
|
|||||||
return res.status(400).json({ success: false, error: 'Payment not completed' });
|
return res.status(400).json({ success: false, error: 'Payment not completed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
addCode(pkg.durationMinutes); // record for audit
|
db.addCode(pkg.durationMinutes); // record for audit
|
||||||
|
|
||||||
const amount = `${Number(pkg.price).toFixed(2)} ${pkg.currency}`;
|
const amount = `${Number(pkg.price).toFixed(2)} ${pkg.currency}`;
|
||||||
const durationText = pkg.durationMinutes >= 1440
|
const durationText = pkg.durationMinutes >= 1440
|
||||||
|
|||||||
Reference in New Issue
Block a user