From 68dd918d3116a0c4b976d8f8aa2dcd70426a20b0 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Fri, 8 Aug 2025 19:38:10 +0200 Subject: [PATCH 1/7] Refactored print.js, bulk-print.ejs, details.ejs, email.ejs, print.ejs, voucher.ejs and size.js for object compatibility with the UniFi Integration API. Updated the unifi.js module to implement the UniFi Integration API. Added the `UNIFI_TOKEN` to variables.js. Added fetch.js util. Check for undefined state in notes.js. Added undici to the dependencies. server.js refactored for compatibility with the UniFi integration API. Fixed incorrect quote filter within server.js. Temporary fixed guest mapping to voucher_code instead of ids --- modules/print.js | 32 ++--- modules/unifi.js | 185 +++++++++++------------------ modules/variables.js | 1 + package-lock.json | 24 +++- package.json | 3 +- server.js | 58 ++++----- template/components/bulk-print.ejs | 28 ++--- template/components/details.ejs | 30 ++--- template/components/email.ejs | 2 +- template/components/print.ejs | 2 +- template/email/voucher.ejs | 16 +-- template/voucher.ejs | 32 ++--- utils/fetch.js | 158 ++++++++++++++++++++++++ utils/notes.js | 2 +- utils/size.js | 6 +- 15 files changed, 358 insertions(+), 221 deletions(-) create mode 100644 utils/fetch.js diff --git a/modules/print.js b/modules/print.js index 6283cd1..3068a48 100644 --- a/modules/print.js +++ b/modules/print.js @@ -171,7 +171,7 @@ module.exports = { }); doc.font('Roboto-Regular') .fontSize(10) - .text(vouchers[item].quota === 1 ? t('singleUse') : vouchers[item].quota === 0 ? t('multiUse') : t('multiUse')); + .text(!vouchers[item].authorizedGuestLimit ? t('multiUse') : vouchers[item].authorizedGuestLimit === 1 ? t('singleUse') : t('multiUse')); doc.font('Roboto-Bold') .fontSize(10) @@ -180,9 +180,9 @@ module.exports = { }); doc.font('Roboto-Regular') .fontSize(10) - .text(time(vouchers[item].duration, language)); + .text(time(vouchers[item].timeLimitMinutes, language)); - if (vouchers[item].qos_usage_quota) { + if (vouchers[item].dataUsageLimitMBytes) { doc.font('Roboto-Bold') .fontSize(10) .text(`${t('dataLimit')}: `, { @@ -190,10 +190,10 @@ module.exports = { }); doc.font('Roboto-Regular') .fontSize(10) - .text(`${bytes(vouchers[item].qos_usage_quota, 2)}`); + .text(`${bytes(vouchers[item].dataUsageLimitMBytes, 2)}`); } - if (vouchers[item].qos_rate_max_down) { + if (vouchers[item].rxRateLimitKbps) { doc.font('Roboto-Bold') .fontSize(10) .text(`${t('downloadLimit')}: `, { @@ -201,10 +201,10 @@ module.exports = { }); doc.font('Roboto-Regular') .fontSize(10) - .text(`${bytes(vouchers[item].qos_rate_max_down, 1, true)}`); + .text(`${bytes(vouchers[item].rxRateLimitKbps, 1, true)}`); } - if (vouchers[item].qos_rate_max_up) { + if (vouchers[item].txRateLimitKbps) { doc.font('Roboto-Bold') .fontSize(10) .text(`${t('uploadLimit')}: `, { @@ -212,7 +212,7 @@ module.exports = { }); doc.font('Roboto-Regular') .fontSize(10) - .text(`${bytes(vouchers[item].qos_rate_max_up, 1, true)}`); + .text(`${bytes(vouchers[item].txRateLimitKbps, 1, true)}`); } } @@ -303,40 +303,40 @@ module.exports = { printer.invert(true); printer.print(`${t('type')}:`); printer.invert(false); - printer.print(voucher.quota === 1 ? ` ${t('singleUse')}` : voucher.quota === 0 ? ` ${t('multiUse')}` : ` ${t('multiUse')}`); + printer.print(!voucher.authorizedGuestLimit ? ` ${t('multiUse')}` : voucher.authorizedGuestLimit === 1 ? ` ${t('singleUse')}` : ` ${t('multiUse')}`); printer.newLine(); printer.setTextDoubleHeight(); printer.invert(true); printer.print(`${t('duration')}:`); printer.invert(false); - printer.print(` ${time(voucher.duration, language)}`); + printer.print(` ${time(voucher.timeLimitMinutes, language)}`); printer.newLine(); - if(voucher.qos_usage_quota) { + if(voucher.dataUsageLimitMBytes) { printer.setTextDoubleHeight(); printer.invert(true); printer.print(`${t('dataLimit')}:`); printer.invert(false); - printer.print(` ${bytes(voucher.qos_usage_quota, 2)}`); + printer.print(` ${bytes(voucher.dataUsageLimitMBytes, 2)}`); printer.newLine(); } - if(voucher.qos_rate_max_down) { + if(voucher.rxRateLimitKbps) { printer.setTextDoubleHeight(); printer.invert(true); printer.print(`${t('downloadLimit')}:`); printer.invert(false); - printer.print(` ${bytes(voucher.qos_rate_max_down, 1, true)}`); + printer.print(` ${bytes(voucher.rxRateLimitKbps, 1, true)}`); printer.newLine(); } - if(voucher.qos_rate_max_up) { + if(voucher.txRateLimitKbps) { printer.setTextDoubleHeight(); printer.invert(true); printer.print(`${t('uploadLimit')}:`); printer.invert(false); - printer.print(` ${bytes(voucher.qos_rate_max_up, 1, true)}`); + printer.print(` ${bytes(voucher.txRateLimitKbps, 1, true)}`); printer.newLine(); } diff --git a/modules/unifi.js b/modules/unifi.js index 4245474..9f84fc0 100644 --- a/modules/unifi.js +++ b/modules/unifi.js @@ -8,6 +8,7 @@ const unifi = require('node-unifi'); */ const variables = require('./variables'); const log = require('./log'); +const fetch = require('../utils/fetch'); /** * UniFi Settings @@ -68,7 +69,7 @@ const startSession = () => { /** * UniFi module functions * - * @type {{create: (function(*, number=, null=, boolean=): Promise<*>), remove: (function(*, boolean=): Promise<*>), list: (function(boolean=): Promise<*>), guests: (function(boolean=): Promise<*>)}} + * @type {{create: (function(*, number=, string=): Promise<*>), remove: (function(*): Promise<*>), list: (function(): Promise<*>), guests: (function(boolean=): Promise<*>)}} */ const unifiModule = { /** @@ -77,57 +78,50 @@ const unifiModule = { * @param type * @param amount * @param note - * @param retry * @return {Promise} */ - create: (type, amount = 1, note = null, retry = true) => { + create: (type, amount = 1, note = '') => { return new Promise((resolve, reject) => { - startSession().then(() => { - controller.createVouchers(type.expiration, amount, parseInt(type.usage), note, typeof type.upload !== "undefined" ? type.upload : null, typeof type.download !== "undefined" ? type.download : null, typeof type.megabytes !== "undefined" ? type.megabytes : null).then((voucher_data) => { - if(amount > 1) { - log.info(`[UniFi] Created ${amount} vouchers`); - resolve(true); - } else { - controller.getVouchers(voucher_data[0].create_time).then((voucher_data_complete) => { - const voucher = `${[voucher_data_complete[0].code.slice(0, 5), '-', voucher_data_complete[0].code.slice(5)].join('')}`; - log.info(`[UniFi] Created voucher with code: ${voucher}`); - resolve(voucher); - }).catch((e) => { - log.error('[UniFi] Error while getting voucher!'); - log.debug(e); - reject('[UniFi] Error while getting voucher!'); - }); - } - }).catch((e) => { - log.error('[UniFi] Error while creating voucher!'); - log.debug(e); + // Set base voucher data + const data = { + count: amount, + name: note, + timeLimitMinutes: type.expiration + }; - // Check if token expired, if true attempt login then try again - if (e.response) { - if(e.response.status === 401 && retry) { - log.info('[UniFi] Attempting re-authentication & retry...'); + // Set voucher limit usage if limited + if(parseInt(type.usage) !== 0) { + data.authorizedGuestLimit = parseInt(type.usage); + } - controller = null; - unifiModule.create(type, amount, note, false).then((e) => { - resolve(e); - }).catch((e) => { - reject(e); - }); - } else { - // Something else went wrong lets clear the current controller so a user can retry - log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); - controller = null; - reject('[UniFi] Error while creating voucher!'); - } - } else { - // Something else went wrong lets clear the current controller so a user can retry - log.error('[UniFi] Unexpected cleanup controller...'); - controller = null; - reject('[UniFi] Error while creating voucher!'); - } - }); + // Set data usage limit if limited + if(typeof type.megabytes !== "undefined") { + data.dataUsageLimitMBytes = type.megabytes; + } + + // Set download speed limit if limited + if(typeof type.download !== "undefined") { + data.rxRateLimitKbps = type.download; + } + + // Set upload speed limit if limited + if(typeof type.upload !== "undefined") { + data.txRateLimitKbps = type.upload; + } + + fetch(`/hotspot/vouchers`, 'POST', {}, data).then((response) => { + if(amount > 1) { + log.info(`[UniFi] Created ${amount} vouchers`); + resolve(true); + } else { + const voucherCode = `${[response.vouchers[0].code.slice(0, 5), '-', response.vouchers[0].code.slice(5)].join('')}`; + log.info(`[UniFi] Created voucher with code: ${voucherCode}`); + resolve(voucherCode); + } }).catch((e) => { - reject(e); + log.error('[UniFi] Error while creating voucher!'); + log.debug(e); + reject('[UniFi] Error while creating voucher!'); }); }); }, @@ -136,44 +130,17 @@ const unifiModule = { * Removes a UniFi Voucher * * @param id - * @param retry * @return {Promise} */ - remove: (id, retry = true) => { + remove: (id) => { return new Promise((resolve, reject) => { - startSession().then(() => { - controller.revokeVoucher(id).then(() => { - resolve(true); - }).catch((e) => { - log.error('[UniFi] Error while removing voucher!'); - log.debug(e); - - // Check if token expired, if true attempt login then try again - if (e.response) { - if(e.response.status === 401 && retry) { - log.info('[UniFi] Attempting re-authentication & retry...'); - - controller = null; - unifiModule.remove(id, false).then((e) => { - resolve(e); - }).catch((e) => { - reject(e); - }); - } else { - // Something else went wrong lets clear the current controller so a user can retry - log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); - controller = null; - reject('[UniFi] Error while removing voucher!'); - } - } else { - // Something else went wrong lets clear the current controller so a user can retry - log.error('[UniFi] Unexpected cleanup controller...'); - controller = null; - reject('[UniFi] Error while removing voucher!'); - } - }); + fetch(`/hotspot/vouchers/${id}`, 'DELETE').then(() => { + log.info(`[UniFi] Deleted voucher: ${id}`); + resolve(true); }).catch((e) => { - reject(e); + log.error('[UniFi] Error while removing voucher!'); + log.debug(e); + reject('[UniFi] Error while removing voucher!'); }); }); }, @@ -181,45 +148,22 @@ const unifiModule = { /** * Returns a list with all UniFi Vouchers * - * @param retry * @return {Promise} */ - list: (retry = true) => { + list: () => { return new Promise((resolve, reject) => { - startSession().then(() => { - controller.getVouchers().then((vouchers) => { - log.info(`[UniFi] Found ${vouchers.length} voucher(s)`); - resolve(vouchers); - }).catch((e) => { - log.error('[UniFi] Error while getting vouchers!'); - log.debug(e); - - // Check if token expired, if true attempt login then try again - if (e.response) { - if(e.response.status === 401 && retry) { - log.info('[UniFi] Attempting re-authentication & retry...'); - - controller = null; - unifiModule.list(false).then((e) => { - resolve(e); - }).catch((e) => { - reject(e); - }); - } else { - // Something else went wrong lets clear the current controller so a user can retry - log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); - controller = null; - reject('[UniFi] Error while getting vouchers!'); - } - } else { - // Something else went wrong lets clear the current controller so a user can retry - log.error('[UniFi] Unexpected cleanup controller...'); - controller = null; - reject('[UniFi] Error while getting vouchers!'); - } - }); + fetch('/hotspot/vouchers', 'GET', { + limit: 10000 + }).then((vouchers) => { + log.info(`[UniFi] Found ${vouchers.length} voucher(s)`); + resolve(vouchers.sort((a, b) => { + if (a.createdAt > b.createdAt) return -1; + if (a.createdAt < b.createdAt) return 1; + })); }).catch((e) => { - reject(e); + log.error('[UniFi] Error while getting vouchers!'); + log.debug(e); + reject('[UniFi] Error while getting vouchers!'); }); }); }, @@ -232,6 +176,17 @@ const unifiModule = { */ guests: (retry = true) => { return new Promise((resolve, reject) => { + // fetch('/clients', 'GET', { + // filter: 'access.type.eq(\'GUEST\')', + // limit: 10000 + // }).then((clients) => { + // console.log(clients) + // }).catch((e) => { + // log.error('[UniFi] Error while getting vouchers!'); + // log.debug(e); + // reject('[UniFi] Error while getting vouchers!'); + // }); + startSession().then(() => { controller.getGuests().then((guests) => { log.info(`[UniFi] Found ${guests.length} guest(s)`); diff --git a/modules/variables.js b/modules/variables.js index 1696295..dbcec21 100644 --- a/modules/variables.js +++ b/modules/variables.js @@ -16,6 +16,7 @@ module.exports = { unifiPort: config('unifi_port') || process.env.UNIFI_PORT || 443, unifiUsername: config('unifi_username') || process.env.UNIFI_USERNAME || 'admin', unifiPassword: config('unifi_password') || process.env.UNIFI_PASSWORD || 'password', + unifiToken: config('unifi_token') || process.env.UNIFI_TOKEN || '', unifiSiteId: config('unifi_site_id') || process.env.UNIFI_SITE_ID || 'default', unifiSsid: config('unifi_ssid') || process.env.UNIFI_SSID || '', unifiSsidPassword: config('unifi_ssid_password') || process.env.UNIFI_SSID_PASSWORD || '', diff --git a/package-lock.json b/package-lock.json index 2fd2a2a..747516b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "node-unifi": "^2.5.1", "nodemailer": "^7.0.5", "pdfkit": "^0.17.1", - "qrcode": "^1.5.4" + "qrcode": "^1.5.4", + "undici": "^5.29.0" }, "devDependencies": { "@tailwindcss/cli": "^4.1.11", @@ -46,6 +47,15 @@ "node": ">=6.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -3699,6 +3709,18 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index 0187587..25686af 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "node-unifi": "^2.5.1", "nodemailer": "^7.0.5", "pdfkit": "^0.17.1", - "qrcode": "^1.5.4" + "qrcode": "^1.5.4", + "undici": "^5.29.0" }, "devDependencies": { "@tailwindcss/cli": "^4.1.11", diff --git a/server.js b/server.js index 778a7f7..5d9297d 100644 --- a/server.js +++ b/server.js @@ -187,7 +187,7 @@ if(variables.serviceWeb) { // Get voucher from cache const voucher = cache.vouchers.find((e) => { - return e._id === req.body.id; + return e.id === req.body.id; }); if(voucher) { @@ -275,7 +275,7 @@ if(variables.serviceWeb) { unifiSsid: variables.unifiSsid, unifiSsidPassword: variables.unifiSsidPassword, qr: await qr(), - voucherId: voucherData._id, + voucherId: voucherData.id, voucherCode }); } @@ -410,7 +410,7 @@ if(variables.serviceWeb) { } const voucher = cache.vouchers.find((e) => { - return e._id === req.params.id; + return e.id === req.params.id; }); if(voucher) { @@ -441,7 +441,7 @@ if(variables.serviceWeb) { } const voucher = cache.vouchers.find((e) => { - return e._id === req.params.id; + return e.id === req.params.id; }); if(voucher) { @@ -476,7 +476,7 @@ if(variables.serviceWeb) { } const voucher = cache.vouchers.find((e) => { - return e._id === req.params.id; + return e.id === req.params.id; }); if(voucher) { @@ -506,7 +506,7 @@ if(variables.serviceWeb) { } const voucher = cache.vouchers.find((e) => { - return e._id === req.params.id; + return e.id === req.params.id; }); if(voucher) { @@ -580,31 +580,31 @@ if(variables.serviceWeb) { voucher_custom: variables.voucherCustom, vouchers: cache.vouchers.filter((item) => { if(variables.authOidcRestrictVisibility && req.oidc) { - return notes(item.note).auth_oidc_domain === user.email.split('@')[1].toLowerCase(); + return item.name && notes(item.name).auth_oidc_domain === user.email.split('@')[1].toLowerCase(); } return true; }).filter((item) => { if(req.query.status === 'available') { - return item.used === 0 && item.status !== 'EXPIRED'; + return item.authorizedGuestCount === 0 && !item.expired; } if(req.query.status === 'in-use') { - return item.used > 0 && item.status !== 'EXPIRED'; + return item.authorizedGuestCount > 0 && !item.expired; } if(req.query.status === 'expired') { - return item.status === 'EXPIRED'; + return item.expired; } return true; }).filter((item) => { if(req.query.quota === 'multi-use') { - return item.quota === 0; + return (item.authorizedGuestLimit && item.authorizedGuestLimit > 1) || !item.authorizedGuestLimit; } if(req.query.quota === 'single-use') { - return item.quota !== 0; + return item.authorizedGuestLimit && item.authorizedGuestLimit === 1; } return true; @@ -615,18 +615,18 @@ if(variables.serviceWeb) { } if(req.query.sort === 'note') { - if ((notes(a.note).note || '') > (notes(b.note).note || '')) return -1; - if ((notes(a.note).note || '') < (notes(b.note).note || '')) return 1; + if ((notes(a.name).note || '') > (notes(b.name).note || '')) return -1; + if ((notes(a.name).note || '') < (notes(b.name).note || '')) return 1; } if(req.query.sort === 'duration') { - if (a.duration > b.duration) return -1; - if (a.duration < b.duration) return 1; + if (a.timeLimitMinutes > b.timeLimitMinutes) return -1; + if (a.timeLimitMinutes < b.timeLimitMinutes) return 1; } if(req.query.sort === 'status') { - if (a.used > b.used) return -1; - if (a.used < b.used) return 1; + if (a.authorizedGuestCount > b.authorizedGuestCount) return -1; + if (a.authorizedGuestCount < b.authorizedGuestCount) return 1; } }), updated: cache.updated, @@ -639,10 +639,10 @@ if(variables.serviceWeb) { }); app.get('/voucher/:id', [authorization.web], async (req, res) => { const voucher = cache.vouchers.find((e) => { - return e._id === req.params.id; + return e.id === req.params.id; }); const guests = cache.guests.filter((e) => { - return e.voucher_id === req.params.id; + return e.voucher_code === voucher.code; }); if(voucher) { @@ -717,7 +717,7 @@ if(variables.serviceWeb) { const vouchers = req.body.vouchers.map((voucher) => { return cache.vouchers.find((e) => { - return e._id === voucher; + return e.id === voucher; }); }); @@ -818,13 +818,13 @@ if(variables.serviceApi) { message: 'OK', vouchers: cache.vouchers.map((voucher) => { return { - id: voucher._id, + id: voucher.id, code: `${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`, - type: voucher.quota === 1 ? 'single' : voucher.quota === 0 ? 'multi' : 'multi', - duration: voucher.duration, - data_limit: voucher.qos_usage_quota ? voucher.qos_usage_quota : null, - download_limit: voucher.qos_rate_max_down ? voucher.qos_rate_max_down : null, - upload_limit: voucher.qos_rate_max_up ? voucher.qos_rate_max_up : null + type: !voucher.authorizedGuestLimit ? 'multi' : voucher.authorizedGuestLimit === 1 ? 'single' : 'multi', + duration: voucher.timeLimitMinutes, + data_limit: voucher.dataUsageLimitMBytes ? voucher.dataUsageLimitMBytes : null, + download_limit: voucher.rxRateLimitKbps ? voucher.rxRateLimitKbps : null, + upload_limit: voucher.txRateLimitKbps ? voucher.txRateLimitKbps : null }; }), updated: cache.updated @@ -920,7 +920,7 @@ if(variables.serviceApi) { data: { message: 'OK', voucher: { - id: voucherData._id, + id: voucherData.id, code: voucherCode }, email: { @@ -936,7 +936,7 @@ if(variables.serviceApi) { data: { message: 'OK', voucher: { - id: voucherData._id, + id: voucherData.id, code: voucherCode } } diff --git a/template/components/bulk-print.ejs b/template/components/bulk-print.ejs index b5f4990..26d95ef 100644 --- a/template/components/bulk-print.ejs +++ b/template/components/bulk-print.ejs @@ -43,18 +43,18 @@