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

This commit is contained in:
Glenn de Haan
2025-08-08 19:38:10 +02:00
parent 21f5be6a0a
commit 68dd918d31
15 changed files with 358 additions and 221 deletions

View File

@@ -171,7 +171,7 @@ module.exports = {
}); });
doc.font('Roboto-Regular') doc.font('Roboto-Regular')
.fontSize(10) .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') doc.font('Roboto-Bold')
.fontSize(10) .fontSize(10)
@@ -180,9 +180,9 @@ module.exports = {
}); });
doc.font('Roboto-Regular') doc.font('Roboto-Regular')
.fontSize(10) .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') doc.font('Roboto-Bold')
.fontSize(10) .fontSize(10)
.text(`${t('dataLimit')}: `, { .text(`${t('dataLimit')}: `, {
@@ -190,10 +190,10 @@ module.exports = {
}); });
doc.font('Roboto-Regular') doc.font('Roboto-Regular')
.fontSize(10) .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') doc.font('Roboto-Bold')
.fontSize(10) .fontSize(10)
.text(`${t('downloadLimit')}: `, { .text(`${t('downloadLimit')}: `, {
@@ -201,10 +201,10 @@ module.exports = {
}); });
doc.font('Roboto-Regular') doc.font('Roboto-Regular')
.fontSize(10) .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') doc.font('Roboto-Bold')
.fontSize(10) .fontSize(10)
.text(`${t('uploadLimit')}: `, { .text(`${t('uploadLimit')}: `, {
@@ -212,7 +212,7 @@ module.exports = {
}); });
doc.font('Roboto-Regular') doc.font('Roboto-Regular')
.fontSize(10) .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.invert(true);
printer.print(`${t('type')}:`); printer.print(`${t('type')}:`);
printer.invert(false); 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.newLine();
printer.setTextDoubleHeight(); printer.setTextDoubleHeight();
printer.invert(true); printer.invert(true);
printer.print(`${t('duration')}:`); printer.print(`${t('duration')}:`);
printer.invert(false); printer.invert(false);
printer.print(` ${time(voucher.duration, language)}`); printer.print(` ${time(voucher.timeLimitMinutes, language)}`);
printer.newLine(); printer.newLine();
if(voucher.qos_usage_quota) { if(voucher.dataUsageLimitMBytes) {
printer.setTextDoubleHeight(); printer.setTextDoubleHeight();
printer.invert(true); printer.invert(true);
printer.print(`${t('dataLimit')}:`); printer.print(`${t('dataLimit')}:`);
printer.invert(false); printer.invert(false);
printer.print(` ${bytes(voucher.qos_usage_quota, 2)}`); printer.print(` ${bytes(voucher.dataUsageLimitMBytes, 2)}`);
printer.newLine(); printer.newLine();
} }
if(voucher.qos_rate_max_down) { if(voucher.rxRateLimitKbps) {
printer.setTextDoubleHeight(); printer.setTextDoubleHeight();
printer.invert(true); printer.invert(true);
printer.print(`${t('downloadLimit')}:`); printer.print(`${t('downloadLimit')}:`);
printer.invert(false); printer.invert(false);
printer.print(` ${bytes(voucher.qos_rate_max_down, 1, true)}`); printer.print(` ${bytes(voucher.rxRateLimitKbps, 1, true)}`);
printer.newLine(); printer.newLine();
} }
if(voucher.qos_rate_max_up) { if(voucher.txRateLimitKbps) {
printer.setTextDoubleHeight(); printer.setTextDoubleHeight();
printer.invert(true); printer.invert(true);
printer.print(`${t('uploadLimit')}:`); printer.print(`${t('uploadLimit')}:`);
printer.invert(false); printer.invert(false);
printer.print(` ${bytes(voucher.qos_rate_max_up, 1, true)}`); printer.print(` ${bytes(voucher.txRateLimitKbps, 1, true)}`);
printer.newLine(); printer.newLine();
} }

View File

@@ -8,6 +8,7 @@ const unifi = require('node-unifi');
*/ */
const variables = require('./variables'); const variables = require('./variables');
const log = require('./log'); const log = require('./log');
const fetch = require('../utils/fetch');
/** /**
* UniFi Settings * UniFi Settings
@@ -68,7 +69,7 @@ const startSession = () => {
/** /**
* UniFi module functions * 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 = { const unifiModule = {
/** /**
@@ -77,57 +78,50 @@ const unifiModule = {
* @param type * @param type
* @param amount * @param amount
* @param note * @param note
* @param retry
* @return {Promise<unknown>} * @return {Promise<unknown>}
*/ */
create: (type, amount = 1, note = null, retry = true) => { create: (type, amount = 1, note = '') => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
startSession().then(() => { // Set base voucher data
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) => { const data = {
if(amount > 1) { count: amount,
log.info(`[UniFi] Created ${amount} vouchers`); name: note,
resolve(true); timeLimitMinutes: type.expiration
} 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);
// Check if token expired, if true attempt login then try again // Set voucher limit usage if limited
if (e.response) { if(parseInt(type.usage) !== 0) {
if(e.response.status === 401 && retry) { data.authorizedGuestLimit = parseInt(type.usage);
log.info('[UniFi] Attempting re-authentication & retry...'); }
controller = null; // Set data usage limit if limited
unifiModule.create(type, amount, note, false).then((e) => { if(typeof type.megabytes !== "undefined") {
resolve(e); data.dataUsageLimitMBytes = type.megabytes;
}).catch((e) => { }
reject(e);
}); // Set download speed limit if limited
} else { if(typeof type.download !== "undefined") {
// Something else went wrong lets clear the current controller so a user can retry data.rxRateLimitKbps = type.download;
log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`); }
controller = null;
reject('[UniFi] Error while creating voucher!'); // Set upload speed limit if limited
} if(typeof type.upload !== "undefined") {
} else { data.txRateLimitKbps = type.upload;
// Something else went wrong lets clear the current controller so a user can retry }
log.error('[UniFi] Unexpected cleanup controller...');
controller = null; fetch(`/hotspot/vouchers`, 'POST', {}, data).then((response) => {
reject('[UniFi] Error while creating voucher!'); 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) => { }).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 * Removes a UniFi Voucher
* *
* @param id * @param id
* @param retry
* @return {Promise<unknown>} * @return {Promise<unknown>}
*/ */
remove: (id, retry = true) => { remove: (id) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
startSession().then(() => { fetch(`/hotspot/vouchers/${id}`, 'DELETE').then(() => {
controller.revokeVoucher(id).then(() => { log.info(`[UniFi] Deleted voucher: ${id}`);
resolve(true); 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!');
}
});
}).catch((e) => { }).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 * Returns a list with all UniFi Vouchers
* *
* @param retry
* @return {Promise<unknown>} * @return {Promise<unknown>}
*/ */
list: (retry = true) => { list: () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
startSession().then(() => { fetch('/hotspot/vouchers', 'GET', {
controller.getVouchers().then((vouchers) => { limit: 10000
log.info(`[UniFi] Found ${vouchers.length} voucher(s)`); }).then((vouchers) => {
resolve(vouchers); log.info(`[UniFi] Found ${vouchers.length} voucher(s)`);
}).catch((e) => { resolve(vouchers.sort((a, b) => {
log.error('[UniFi] Error while getting vouchers!'); if (a.createdAt > b.createdAt) return -1;
log.debug(e); if (a.createdAt < b.createdAt) return 1;
}));
// 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!');
}
});
}).catch((e) => { }).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) => { guests: (retry = true) => {
return new Promise((resolve, reject) => { 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(() => { startSession().then(() => {
controller.getGuests().then((guests) => { controller.getGuests().then((guests) => {
log.info(`[UniFi] Found ${guests.length} guest(s)`); log.info(`[UniFi] Found ${guests.length} guest(s)`);

View File

@@ -16,6 +16,7 @@ module.exports = {
unifiPort: config('unifi_port') || process.env.UNIFI_PORT || 443, unifiPort: config('unifi_port') || process.env.UNIFI_PORT || 443,
unifiUsername: config('unifi_username') || process.env.UNIFI_USERNAME || 'admin', unifiUsername: config('unifi_username') || process.env.UNIFI_USERNAME || 'admin',
unifiPassword: config('unifi_password') || process.env.UNIFI_PASSWORD || 'password', 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', unifiSiteId: config('unifi_site_id') || process.env.UNIFI_SITE_ID || 'default',
unifiSsid: config('unifi_ssid') || process.env.UNIFI_SSID || '', unifiSsid: config('unifi_ssid') || process.env.UNIFI_SSID || '',
unifiSsidPassword: config('unifi_ssid_password') || process.env.UNIFI_SSID_PASSWORD || '', unifiSsidPassword: config('unifi_ssid_password') || process.env.UNIFI_SSID_PASSWORD || '',

24
package-lock.json generated
View File

@@ -21,7 +21,8 @@
"node-unifi": "^2.5.1", "node-unifi": "^2.5.1",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"pdfkit": "^0.17.1", "pdfkit": "^0.17.1",
"qrcode": "^1.5.4" "qrcode": "^1.5.4",
"undici": "^5.29.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/cli": "^4.1.11", "@tailwindcss/cli": "^4.1.11",
@@ -46,6 +47,15 @@
"node": ">=6.0.0" "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": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -3699,6 +3709,18 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT" "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": { "node_modules/undici-types": {
"version": "6.19.8", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",

View File

@@ -33,7 +33,8 @@
"node-unifi": "^2.5.1", "node-unifi": "^2.5.1",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"pdfkit": "^0.17.1", "pdfkit": "^0.17.1",
"qrcode": "^1.5.4" "qrcode": "^1.5.4",
"undici": "^5.29.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/cli": "^4.1.11", "@tailwindcss/cli": "^4.1.11",

View File

@@ -187,7 +187,7 @@ if(variables.serviceWeb) {
// Get voucher from cache // Get voucher from cache
const voucher = cache.vouchers.find((e) => { const voucher = cache.vouchers.find((e) => {
return e._id === req.body.id; return e.id === req.body.id;
}); });
if(voucher) { if(voucher) {
@@ -275,7 +275,7 @@ if(variables.serviceWeb) {
unifiSsid: variables.unifiSsid, unifiSsid: variables.unifiSsid,
unifiSsidPassword: variables.unifiSsidPassword, unifiSsidPassword: variables.unifiSsidPassword,
qr: await qr(), qr: await qr(),
voucherId: voucherData._id, voucherId: voucherData.id,
voucherCode voucherCode
}); });
} }
@@ -410,7 +410,7 @@ if(variables.serviceWeb) {
} }
const voucher = cache.vouchers.find((e) => { const voucher = cache.vouchers.find((e) => {
return e._id === req.params.id; return e.id === req.params.id;
}); });
if(voucher) { if(voucher) {
@@ -441,7 +441,7 @@ if(variables.serviceWeb) {
} }
const voucher = cache.vouchers.find((e) => { const voucher = cache.vouchers.find((e) => {
return e._id === req.params.id; return e.id === req.params.id;
}); });
if(voucher) { if(voucher) {
@@ -476,7 +476,7 @@ if(variables.serviceWeb) {
} }
const voucher = cache.vouchers.find((e) => { const voucher = cache.vouchers.find((e) => {
return e._id === req.params.id; return e.id === req.params.id;
}); });
if(voucher) { if(voucher) {
@@ -506,7 +506,7 @@ if(variables.serviceWeb) {
} }
const voucher = cache.vouchers.find((e) => { const voucher = cache.vouchers.find((e) => {
return e._id === req.params.id; return e.id === req.params.id;
}); });
if(voucher) { if(voucher) {
@@ -580,31 +580,31 @@ if(variables.serviceWeb) {
voucher_custom: variables.voucherCustom, voucher_custom: variables.voucherCustom,
vouchers: cache.vouchers.filter((item) => { vouchers: cache.vouchers.filter((item) => {
if(variables.authOidcRestrictVisibility && req.oidc) { 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; return true;
}).filter((item) => { }).filter((item) => {
if(req.query.status === 'available') { if(req.query.status === 'available') {
return item.used === 0 && item.status !== 'EXPIRED'; return item.authorizedGuestCount === 0 && !item.expired;
} }
if(req.query.status === 'in-use') { if(req.query.status === 'in-use') {
return item.used > 0 && item.status !== 'EXPIRED'; return item.authorizedGuestCount > 0 && !item.expired;
} }
if(req.query.status === 'expired') { if(req.query.status === 'expired') {
return item.status === 'EXPIRED'; return item.expired;
} }
return true; return true;
}).filter((item) => { }).filter((item) => {
if(req.query.quota === 'multi-use') { if(req.query.quota === 'multi-use') {
return item.quota === 0; return (item.authorizedGuestLimit && item.authorizedGuestLimit > 1) || !item.authorizedGuestLimit;
} }
if(req.query.quota === 'single-use') { if(req.query.quota === 'single-use') {
return item.quota !== 0; return item.authorizedGuestLimit && item.authorizedGuestLimit === 1;
} }
return true; return true;
@@ -615,18 +615,18 @@ if(variables.serviceWeb) {
} }
if(req.query.sort === 'note') { if(req.query.sort === 'note') {
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.note).note || '') < (notes(b.note).note || '')) return 1; if ((notes(a.name).note || '') < (notes(b.name).note || '')) return 1;
} }
if(req.query.sort === 'duration') { if(req.query.sort === 'duration') {
if (a.duration > b.duration) return -1; if (a.timeLimitMinutes > b.timeLimitMinutes) return -1;
if (a.duration < b.duration) return 1; if (a.timeLimitMinutes < b.timeLimitMinutes) return 1;
} }
if(req.query.sort === 'status') { if(req.query.sort === 'status') {
if (a.used > b.used) return -1; if (a.authorizedGuestCount > b.authorizedGuestCount) return -1;
if (a.used < b.used) return 1; if (a.authorizedGuestCount < b.authorizedGuestCount) return 1;
} }
}), }),
updated: cache.updated, updated: cache.updated,
@@ -639,10 +639,10 @@ if(variables.serviceWeb) {
}); });
app.get('/voucher/:id', [authorization.web], async (req, res) => { app.get('/voucher/:id', [authorization.web], async (req, res) => {
const voucher = cache.vouchers.find((e) => { const voucher = cache.vouchers.find((e) => {
return e._id === req.params.id; return e.id === req.params.id;
}); });
const guests = cache.guests.filter((e) => { const guests = cache.guests.filter((e) => {
return e.voucher_id === req.params.id; return e.voucher_code === voucher.code;
}); });
if(voucher) { if(voucher) {
@@ -717,7 +717,7 @@ if(variables.serviceWeb) {
const vouchers = req.body.vouchers.map((voucher) => { const vouchers = req.body.vouchers.map((voucher) => {
return cache.vouchers.find((e) => { return cache.vouchers.find((e) => {
return e._id === voucher; return e.id === voucher;
}); });
}); });
@@ -818,13 +818,13 @@ if(variables.serviceApi) {
message: 'OK', message: 'OK',
vouchers: cache.vouchers.map((voucher) => { vouchers: cache.vouchers.map((voucher) => {
return { return {
id: voucher._id, id: voucher.id,
code: `${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`, code: `${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`,
type: voucher.quota === 1 ? 'single' : voucher.quota === 0 ? 'multi' : 'multi', type: !voucher.authorizedGuestLimit ? 'multi' : voucher.authorizedGuestLimit === 1 ? 'single' : 'multi',
duration: voucher.duration, duration: voucher.timeLimitMinutes,
data_limit: voucher.qos_usage_quota ? voucher.qos_usage_quota : null, data_limit: voucher.dataUsageLimitMBytes ? voucher.dataUsageLimitMBytes : null,
download_limit: voucher.qos_rate_max_down ? voucher.qos_rate_max_down : null, download_limit: voucher.rxRateLimitKbps ? voucher.rxRateLimitKbps : null,
upload_limit: voucher.qos_rate_max_up ? voucher.qos_rate_max_up : null upload_limit: voucher.txRateLimitKbps ? voucher.txRateLimitKbps : null
}; };
}), }),
updated: cache.updated updated: cache.updated
@@ -920,7 +920,7 @@ if(variables.serviceApi) {
data: { data: {
message: 'OK', message: 'OK',
voucher: { voucher: {
id: voucherData._id, id: voucherData.id,
code: voucherCode code: voucherCode
}, },
email: { email: {
@@ -936,7 +936,7 @@ if(variables.serviceApi) {
data: { data: {
message: 'OK', message: 'OK',
voucher: { voucher: {
id: voucherData._id, id: voucherData.id,
code: voucherCode code: voucherCode
} }
} }

View File

@@ -43,18 +43,18 @@
<ul role="list" class="max-h-96 h-96 overflow-y-auto mt-2 rounded-md border-0 divide-y divide-black/5 dark:divide-white/5 dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10"> <ul role="list" class="max-h-96 h-96 overflow-y-auto mt-2 rounded-md border-0 divide-y divide-black/5 dark:divide-white/5 dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10">
<% vouchers.forEach((voucher) => { %> <% vouchers.forEach((voucher) => { %>
<li class="relative flex items-center space-x-4 px-4 py-4"> <li class="relative flex items-center space-x-4 px-4 py-4">
<input id="voucher-<%= voucher._id %>" aria-describedby="voucher-<%= voucher._id %>" name="vouchers" type="checkbox" value="<%= voucher._id %>" class="col-start-1 row-start-1 appearance-none rounded-sm border-0 dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 focus:ring-offset-0"> <input id="voucher-<%= voucher.id %>" aria-describedby="voucher-<%= voucher.id %>" name="vouchers" type="checkbox" value="<%= voucher.id %>" class="col-start-1 row-start-1 appearance-none rounded-sm border-0 dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 focus:ring-offset-0">
<label for="voucher-<%= voucher._id %>" class="voucher min-w-0 flex-auto"> <label for="voucher-<%= voucher.id %>" class="voucher min-w-0 flex-auto">
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white"> <h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white">
<div class="flex gap-x-2"> <div class="flex gap-x-2">
<span class="tabular-nums pointer-events-none no-underline text-inherit"><%= voucher.code.slice(0, 5) %>-<%= voucher.code.slice(5) %></span> <span class="tabular-nums pointer-events-none no-underline text-inherit"><%= voucher.code.slice(0, 5) %>-<%= voucher.code.slice(5) %></span>
<% if (voucher.status === 'EXPIRED') { %> <% if (voucher.expired) { %>
<div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-red-50 text-red-800 ring-red-600/20 dark:text-red-400 dark:bg-red-400/10 dark:ring-red-400/20"> <div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-red-50 text-red-800 ring-red-600/20 dark:text-red-400 dark:bg-red-400/10 dark:ring-red-400/20">
Expired Expired
</div> </div>
<% } else {%> <% } else {%>
<% if(voucher.used > 0) { %> <% if(voucher.authorizedGuestCount > 0) { %>
<div class="rounded-full flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20"> <div class="rounded-full flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20">
In Use In Use
</div> </div>
@@ -64,37 +64,37 @@
</div> </div>
<% } %> <% } %>
<% } %> <% } %>
<% if (notesConvert(voucher.note).note) { %> <% if (voucher.name && notesConvert(voucher.name).note) { %>
<div class="hidden sm:block rounded-md flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-gray-50 text-gray-800 ring-gray-600/20 dark:text-gray-400 dark:bg-gray-400/10 dark:ring-gray-400/20"> <div class="hidden sm:block rounded-md flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-gray-50 text-gray-800 ring-gray-600/20 dark:text-gray-400 dark:bg-gray-400/10 dark:ring-gray-400/20">
<%= notesConvert(voucher.note).note %> <%= notesConvert(voucher.name).note %>
</div> </div>
<% } %> <% } %>
</div> </div>
</h2> </h2>
</div> </div>
<div class="mt-2 flex items-center gap-x-2.5 text-xs leading-5 text-gray-600 dark:text-gray-400"> <div class="mt-2 flex items-center gap-x-2.5 text-xs leading-5 text-gray-600 dark:text-gray-400">
<p class="whitespace-nowrap"><%= voucher.quota === 1 ? 'Single-use' : voucher.quota === 0 ? 'Multi-use (Unlimited)' : `Multi-use (${voucher.quota}x)` %></p> <p class="whitespace-nowrap"><%= !voucher.authorizedGuestLimit ? 'Multi-use (Unlimited)' : voucher.authorizedGuestLimit === 1 ? 'Single-use' : `Multi-use (${voucher.authorizedGuestLimit}x)` %></p>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class="whitespace-nowrap"><%= timeConvert(voucher.duration) %></p> <p class="whitespace-nowrap"><%= timeConvert(voucher.timeLimitMinutes) %></p>
<% if(voucher.qos_usage_quota) { %> <% if(voucher.dataUsageLimitMBytes) { %>
<svg viewBox="0 0 2 2" class="hidden sm:block h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="hidden sm:block h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class="hidden sm:block truncate"><%= bytesConvert(voucher.qos_usage_quota, 2) %> Data Limit</p> <p class="hidden sm:block truncate"><%= bytesConvert(voucher.dataUsageLimitMBytes, 2) %> Data Limit</p>
<% } %> <% } %>
<% if(voucher.qos_rate_max_down) { %> <% if(voucher.rxRateLimitKbps) { %>
<svg viewBox="0 0 2 2" class="hidden sm:block h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="hidden sm:block h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class="hidden sm:block truncate"><%= bytesConvert(voucher.qos_rate_max_down, 1, true) %> Download Limit</p> <p class="hidden sm:block truncate"><%= bytesConvert(voucher.rxRateLimitKbps, 1, true) %> Download Limit</p>
<% } %> <% } %>
<% if(voucher.qos_rate_max_up) { %> <% if(voucher.txRateLimitKbps) { %>
<svg viewBox="0 0 2 2" class="hidden sm:block h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="hidden sm:block h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class=" hidden sm:block truncate"><%= bytesConvert(voucher.qos_rate_max_up, 1, true) %> Upload Limit</p> <p class=" hidden sm:block truncate"><%= bytesConvert(voucher.txRateLimitKbps, 1, true) %> Upload Limit</p>
<% } %> <% } %>
</div> </div>
</label> </label>

View File

@@ -26,12 +26,12 @@
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Status</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Status</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0">
<% if (voucher.status === 'EXPIRED') { %> <% if (voucher.expired) { %>
<div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-red-50 text-red-800 ring-red-600/20 dark:text-red-400 dark:bg-red-400/10 dark:ring-red-400/20"> <div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-red-50 text-red-800 ring-red-600/20 dark:text-red-400 dark:bg-red-400/10 dark:ring-red-400/20">
Expired Expired
</div> </div>
<% } else {%> <% } else {%>
<% if(voucher.used > 0) { %> <% if(voucher.authorizedGuestCount > 0) { %>
<div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20"> <div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20">
In Use In Use
</div> </div>
@@ -43,49 +43,49 @@
<% } %> <% } %>
</dd> </dd>
</div> </div>
<% if(notesConvert(voucher.note).note) { %> <% if(voucher.name && notesConvert(voucher.name).note) { %>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Notes</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Notes</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.note).note %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.name).note %></dd>
</div> </div>
<% } %> <% } %>
<% if(notesConvert(voucher.note).source) { %> <% if(voucher.name && notesConvert(voucher.name).source) { %>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Source</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Source</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.note).source %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.name).source %></dd>
</div> </div>
<% } %> <% } %>
<% if(notesConvert(voucher.note).auth_type) { %> <% if(voucher.name && notesConvert(voucher.name).auth_type) { %>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Authentication</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Authentication</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.note).auth_type %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.name).auth_type %></dd>
</div> </div>
<% } %> <% } %>
<% if(notesConvert(voucher.note).auth_oidc_domain) { %> <% if(voucher.name && notesConvert(voucher.name).auth_oidc_domain) { %>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">OIDC Domain</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">OIDC Domain</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.note).auth_oidc_domain %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= notesConvert(voucher.name).auth_oidc_domain %></dd>
</div> </div>
<% } %> <% } %>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Type</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Type</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.quota === 1 ? 'Single-use' : voucher.quota === 0 ? 'Multi-use (Unlimited)' : `Multi-use (${voucher.quota}x)` %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= !voucher.authorizedGuestLimit ? 'Multi-use (Unlimited)' : voucher.authorizedGuestLimit === 1 ? 'Single-use' : `Multi-use (${voucher.authorizedGuestLimit}x)` %></dd>
</div> </div>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Duration</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Duration</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= timeConvert(voucher.duration) %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= timeConvert(voucher.timeLimitMinutes) %></dd>
</div> </div>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Data Limit</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Data Limit</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.qos_usage_quota ? bytesConvert(voucher.qos_usage_quota, 2) : 'Unlimited' %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.dataUsageLimitMBytes ? bytesConvert(voucher.dataUsageLimitMBytes, 2) : 'Unlimited' %></dd>
</div> </div>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Download Limit</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Download Limit</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.qos_rate_max_down ? bytesConvert(voucher.qos_rate_max_down, 1, true) : 'Unlimited' %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.rxRateLimitKbps ? bytesConvert(voucher.rxRateLimitKbps, 1, true) : 'Unlimited' %></dd>
</div> </div>
<div class="py-2 grid grid-cols-3 gap-4 px-0"> <div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Upload Limit</dt> <dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Upload Limit</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.qos_rate_max_up ? bytesConvert(voucher.qos_rate_max_up, 1, true) : 'Unlimited' %></dd> <dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.txRateLimitKbps ? bytesConvert(voucher.txRateLimitKbps, 1, true) : 'Unlimited' %></dd>
</div> </div>
</dl> </dl>
</div> </div>

View File

@@ -5,7 +5,7 @@
<div class="absolute inset-0 overflow-hidden"> <div class="absolute inset-0 overflow-hidden">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16"> <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
<div class="pointer-events-auto w-screen max-w-md"> <div class="pointer-events-auto w-screen max-w-md">
<form id="email-form" class="flex h-full flex-col divide-y divide-black/5 dark:divide-white/5 bg-white dark:bg-gray-900 shadow-xl" action="<%= baseUrl %>/voucher/<%= voucher._id %>/email" method="post" enctype="multipart/form-data"> <form id="email-form" class="flex h-full flex-col divide-y divide-black/5 dark:divide-white/5 bg-white dark:bg-gray-900 shadow-xl" action="<%= baseUrl %>/voucher/<%= voucher.id %>/email" method="post" enctype="multipart/form-data">
<div class="h-0 flex-1 overflow-y-auto"> <div class="h-0 flex-1 overflow-y-auto">
<div class="bg-sky-700 px-4 py-6 sm:px-6"> <div class="bg-sky-700 px-4 py-6 sm:px-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -5,7 +5,7 @@
<div class="absolute inset-0 overflow-hidden"> <div class="absolute inset-0 overflow-hidden">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16"> <div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
<div class="pointer-events-auto w-screen max-w-md"> <div class="pointer-events-auto w-screen max-w-md">
<form id="print-form" class="flex h-full flex-col divide-y divide-black/5 dark:divide-white/5 bg-white dark:bg-gray-900 shadow-xl" action="<%= baseUrl %>/voucher/<%= voucher._id %>/print" method="post" enctype="multipart/form-data"> <form id="print-form" class="flex h-full flex-col divide-y divide-black/5 dark:divide-white/5 bg-white dark:bg-gray-900 shadow-xl" action="<%= baseUrl %>/voucher/<%= voucher.id %>/print" method="post" enctype="multipart/form-data">
<div class="h-0 flex-1 overflow-y-auto"> <div class="h-0 flex-1 overflow-y-auto">
<div class="bg-sky-700 px-4 py-6 sm:px-6"> <div class="bg-sky-700 px-4 py-6 sm:px-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -140,16 +140,16 @@
<% } %> <% } %>
<p style="font-family: sans-serif; font-size: 20px; font-weight: bold; margin: 0; margin-bottom: 15px;"><%= t('details') %></p> <p style="font-family: sans-serif; font-size: 20px; font-weight: bold; margin: 0; margin-bottom: 15px;"><%= t('details') %></p>
<hr/> <hr/>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('type') %>:</span> <%= voucher.quota === 1 ? t('singleUse') : voucher.quota === 0 ? t('multiUse') : t('multiUse') %></p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('type') %>:</span> <%= !voucher.authorizedGuestLimit ? t('multiUse') : voucher.authorizedGuestLimit === 1 ? t('singleUse') : t('multiUse') %></p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('duration') %>:</span> <%= timeConvert(voucher.duration, language) %></p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('duration') %>:</span> <%= timeConvert(voucher.timeLimitMinutes, language) %></p>
<% if(voucher.qos_usage_quota) { %> <% if(voucher.dataUsageLimitMBytes) { %>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('dataLimit') %>:</span> <%= bytesConvert(voucher.qos_usage_quota, 2) %></p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('dataLimit') %>:</span> <%= bytesConvert(voucher.dataUsageLimitMBytes, 2) %></p>
<% } %> <% } %>
<% if(voucher.qos_rate_max_down) { %> <% if(voucher.rxRateLimitKbps) { %>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('downloadLimit') %>:</span> <%= bytesConvert(voucher.qos_rate_max_down, 1, true) %></p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('downloadLimit') %>:</span> <%= bytesConvert(voucher.rxRateLimitKbps, 1, true) %></p>
<% } %> <% } %>
<% if(voucher.qos_rate_max_up) { %> <% if(voucher.txRateLimitKbps) { %>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('uploadLimit') %>:</span> <%= bytesConvert(voucher.qos_rate_max_up, 1, true) %></p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0;"><span style="font-weight: bold;"><%= t('uploadLimit') %>:</span> <%= bytesConvert(voucher.txRateLimitKbps, 1, true) %></p>
<% } %> <% } %>
</td> </td>
</tr> </tr>

View File

@@ -146,17 +146,17 @@
<ul role="list" class="divide-y divide-black/5 dark:divide-white/5"> <ul role="list" class="divide-y divide-black/5 dark:divide-white/5">
<% vouchers.forEach((voucher) => { %> <% vouchers.forEach((voucher) => { %>
<li class="relative flex items-center space-x-4 px-4 py-4 sm:px-6 lg:px-8 hover:bg-gray-200 dark:hover:bg-gray-800 cursor-pointer"> <li class="relative flex items-center space-x-4 px-4 py-4 sm:px-6 lg:px-8 hover:bg-gray-200 dark:hover:bg-gray-800 cursor-pointer">
<div class="voucher min-w-0 flex-auto" data-id="<%= voucher._id %>"> <div class="voucher min-w-0 flex-auto" data-id="<%= voucher.id %>">
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white"> <h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white">
<div class="flex gap-x-2"> <div class="flex gap-x-2">
<span class="tabular-nums pointer-events-none no-underline text-inherit"><%= voucher.code.slice(0, 5) %>-<%= voucher.code.slice(5) %></span> <span class="tabular-nums pointer-events-none no-underline text-inherit"><%= voucher.code.slice(0, 5) %>-<%= voucher.code.slice(5) %></span>
<% if (voucher.status === 'EXPIRED') { %> <% if (voucher.expired) { %>
<div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-red-50 text-red-800 ring-red-600/20 dark:text-red-400 dark:bg-red-400/10 dark:ring-red-400/20"> <div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-red-50 text-red-800 ring-red-600/20 dark:text-red-400 dark:bg-red-400/10 dark:ring-red-400/20">
Expired Expired
</div> </div>
<% } else {%> <% } else {%>
<% if(voucher.used > 0) { %> <% if(voucher.authorizedGuestCount > 0) { %>
<div class="rounded-full flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20"> <div class="rounded-full flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20">
In Use In Use
</div> </div>
@@ -166,37 +166,37 @@
</div> </div>
<% } %> <% } %>
<% } %> <% } %>
<% if (notesConvert(voucher.note).note) { %> <% if (voucher.name && notesConvert(voucher.name).note) { %>
<div class="hidden sm:block rounded-md flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-gray-50 text-gray-800 ring-gray-600/20 dark:text-gray-400 dark:bg-gray-400/10 dark:ring-gray-400/20"> <div class="hidden sm:block rounded-md flex-none py-1 px-2 text-xs font-medium ring-1 ring-inset bg-gray-50 text-gray-800 ring-gray-600/20 dark:text-gray-400 dark:bg-gray-400/10 dark:ring-gray-400/20">
<%= notesConvert(voucher.note).note %> <%= notesConvert(voucher.name).note %>
</div> </div>
<% } %> <% } %>
</div> </div>
</h2> </h2>
</div> </div>
<div class="mt-2 flex items-center gap-x-2.5 text-xs leading-5 text-gray-600 dark:text-gray-400"> <div class="mt-2 flex items-center gap-x-2.5 text-xs leading-5 text-gray-600 dark:text-gray-400">
<p class="whitespace-nowrap"><%= voucher.quota === 1 ? 'Single-use' : voucher.quota === 0 ? 'Multi-use (Unlimited)' : `Multi-use (${voucher.quota}x)` %></p> <p class="whitespace-nowrap"><%= !voucher.authorizedGuestLimit ? 'Multi-use (Unlimited)' : voucher.authorizedGuestLimit === 1 ? 'Single-use' : `Multi-use (${voucher.authorizedGuestLimit}x)` %></p>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class="whitespace-nowrap"><%= timeConvert(voucher.duration) %></p> <p class="whitespace-nowrap"><%= timeConvert(voucher.timeLimitMinutes) %></p>
<% if(voucher.qos_usage_quota) { %> <% if(voucher.dataUsageLimitMBytes) { %>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class="truncate"><%= bytesConvert(voucher.qos_usage_quota, 2) %> Data Limit</p> <p class="truncate"><%= bytesConvert(voucher.dataUsageLimitMBytes, 2) %> Data Limit</p>
<% } %> <% } %>
<% if(voucher.qos_rate_max_down) { %> <% if(voucher.rxRateLimitKbps) { %>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class="truncate"><%= bytesConvert(voucher.qos_rate_max_down, 1, true) %> Download Limit</p> <p class="truncate"><%= bytesConvert(voucher.rxRateLimitKbps, 1, true) %> Download Limit</p>
<% } %> <% } %>
<% if(voucher.qos_rate_max_up) { %> <% if(voucher.txRateLimitKbps) { %>
<svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300"> <svg viewBox="0 0 2 2" class="h-0.5 w-0.5 flex-none fill-gray-500 dark:fill-gray-300">
<circle cx="1" cy="1" r="1" /> <circle cx="1" cy="1" r="1" />
</svg> </svg>
<p class="truncate"><%= bytesConvert(voucher.qos_rate_max_up, 1, true) %> Upload Limit</p> <p class="truncate"><%= bytesConvert(voucher.txRateLimitKbps, 1, true) %> Upload Limit</p>
<% } %> <% } %>
</div> </div>
</div> </div>
@@ -209,7 +209,7 @@
<path d="M10.5 10.5a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963 5.23 5.23 0 0 0-3.434-1.279h-1.875a.375.375 0 0 1-.375-.375V10.5Z" /> <path d="M10.5 10.5a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963 5.23 5.23 0 0 0-3.434-1.279h-1.875a.375.375 0 0 1-.375-.375V10.5Z" />
</svg> </svg>
</button> </button>
<button type="button" data-id="<%= voucher._id %>" class="voucher-email relative rounded-full p-1 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white<%= !email_enabled ? ' hidden' : '' %>"> <button type="button" data-id="<%= voucher.id %>" class="voucher-email relative rounded-full p-1 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white<%= !email_enabled ? ' hidden' : '' %>">
<span class="absolute -inset-1.5"></span> <span class="absolute -inset-1.5"></span>
<span class="sr-only">Email Voucher Code</span> <span class="sr-only">Email Voucher Code</span>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
@@ -217,14 +217,14 @@
<path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" /> <path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" />
</svg> </svg>
</button> </button>
<button type="button" data-id="<%= voucher._id %>" class="voucher-print relative rounded-full p-1 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white<%= !printer_enabled ? ' hidden' : '' %>"> <button type="button" data-id="<%= voucher.id %>" class="voucher-print relative rounded-full p-1 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white<%= !printer_enabled ? ' hidden' : '' %>">
<span class="absolute -inset-1.5"></span> <span class="absolute -inset-1.5"></span>
<span class="sr-only">Print Voucher Code</span> <span class="sr-only">Print Voucher Code</span>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.875 1.5C6.839 1.5 6 2.34 6 3.375v2.99c-.426.053-.851.11-1.274.174-1.454.218-2.476 1.483-2.476 2.917v6.294a3 3 0 0 0 3 3h.27l-.155 1.705A1.875 1.875 0 0 0 7.232 22.5h9.536a1.875 1.875 0 0 0 1.867-2.045l-.155-1.705h.27a3 3 0 0 0 3-3V9.456c0-1.434-1.022-2.7-2.476-2.917A48.716 48.716 0 0 0 18 6.366V3.375c0-1.036-.84-1.875-1.875-1.875h-8.25ZM16.5 6.205v-2.83A.375.375 0 0 0 16.125 3h-8.25a.375.375 0 0 0-.375.375v2.83a49.353 49.353 0 0 1 9 0Zm-.217 8.265c.178.018.317.16.333.337l.526 5.784a.375.375 0 0 1-.374.409H7.232a.375.375 0 0 1-.374-.409l.526-5.784a.373.373 0 0 1 .333-.337 41.741 41.741 0 0 1 8.566 0Zm.967-3.97a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75H18a.75.75 0 0 1-.75-.75V10.5ZM15 9.75a.75.75 0 0 0-.75.75v.008c0 .414.336.75.75.75h.008a.75.75 0 0 0 .75-.75V10.5a.75.75 0 0 0-.75-.75H15Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M7.875 1.5C6.839 1.5 6 2.34 6 3.375v2.99c-.426.053-.851.11-1.274.174-1.454.218-2.476 1.483-2.476 2.917v6.294a3 3 0 0 0 3 3h.27l-.155 1.705A1.875 1.875 0 0 0 7.232 22.5h9.536a1.875 1.875 0 0 0 1.867-2.045l-.155-1.705h.27a3 3 0 0 0 3-3V9.456c0-1.434-1.022-2.7-2.476-2.917A48.716 48.716 0 0 0 18 6.366V3.375c0-1.036-.84-1.875-1.875-1.875h-8.25ZM16.5 6.205v-2.83A.375.375 0 0 0 16.125 3h-8.25a.375.375 0 0 0-.375.375v2.83a49.353 49.353 0 0 1 9 0Zm-.217 8.265c.178.018.317.16.333.337l.526 5.784a.375.375 0 0 1-.374.409H7.232a.375.375 0 0 1-.374-.409l.526-5.784a.373.373 0 0 1 .333-.337 41.741 41.741 0 0 1 8.566 0Zm.967-3.97a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75H18a.75.75 0 0 1-.75-.75V10.5ZM15 9.75a.75.75 0 0 0-.75.75v.008c0 .414.336.75.75.75h.008a.75.75 0 0 0 .75-.75V10.5a.75.75 0 0 0-.75-.75H15Z" clip-rule="evenodd" />
</svg> </svg>
</button> </button>
<a href="<%= baseUrl %>/voucher/<%= voucher._id %>/remove" type="button" class="remove-button relative rounded-full p-1 text-red-500 dark:text-red-400 hover:text-black dark:hover:text-white"> <a href="<%= baseUrl %>/voucher/<%= voucher.id %>/remove" type="button" class="remove-button relative rounded-full p-1 text-red-500 dark:text-red-400 hover:text-black dark:hover:text-white">
<span class="absolute -inset-1.5"></span> <span class="absolute -inset-1.5"></span>
<span class="sr-only">Remove Voucher Code</span> <span class="sr-only">Remove Voucher Code</span>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">

158
utils/fetch.js Normal file
View File

@@ -0,0 +1,158 @@
/**
* Import vendor modules
*/
const querystring = require('node:querystring');
const {Agent} = require('undici');
/**
* Import own modules
*/
const variables = require('../modules/variables');
const log = require('../modules/log');
/**
* UniFi Settings
*/
const controller = {
ip: variables.unifiIp,
port: variables.unifiPort,
username: variables.unifiUsername,
password: variables.unifiPassword,
token: variables.unifiToken,
siteID: variables.unifiSiteId,
siteUUID: null
};
/**
* Request a Controller Site UUID
*
* @returns {Promise<unknown>}
*/
const getSiteUUID = () => {
return new Promise((resolve, reject) => {
fetch(`https://${controller.ip}:${controller.port}/proxy/network/integration/v1/sites?filter=internalReference.eq('${controller.siteID}')`, {
headers: {
'User-Agent': 'unifi-voucher-site',
'Content-Type': 'application/json',
'X-API-KEY': controller.token
},
dispatcher: new Agent({
connect: {
rejectUnauthorized: false
}
})
})
.then((response) => {
return response.json();
})
.then((response) => {
if(response.error) {
log.error(`[UniFi] Error while requesting site uuid. Error: ${response.error.message}`);
log.debug(response.error);
reject(response.error.message);
return;
}
if(response.statusCode) {
log.error(`[UniFi] Error while requesting site uuid. Error: ${response.message}`);
log.debug(response);
reject(response.message);
return;
}
if(response.data.length < 1) {
log.error(`[UniFi] Unknown site id: ${controller.siteID}.`);
log.debug(response);
reject(`Unknown site id: ${controller.siteID}`);
return;
}
log.debug(`[UniFi] Site UUID: ${response.data[0].id}`);
resolve(response.data[0].id);
})
.catch((err) => {
log.error('[UniFi] Error while processing request.');
log.debug(err);
reject(err);
});
});
}
/**
* Fetch util to get data from a UniFi Controller
*
* @param endpoint
* @param method
* @param params
* @param data
* @returns {Promise<unknown>}
*/
module.exports = (endpoint, method = 'GET', params = {}, data = null) => {
return new Promise(async (resolve, reject) => {
// Auto-resolve siteUUID if not set
if(controller.siteUUID === null) {
log.debug('[UniFi] Requesting Site UUID...');
const siteUUID = await getSiteUUID().catch((err) => {
reject(err);
});
if(siteUUID) {
controller.siteUUID = siteUUID;
} else {
return;
}
}
// Define base request
const request = {
method,
headers: {
'User-Agent': 'unifi-voucher-site',
'Content-Type': 'application/json',
'X-API-KEY': controller.token
},
dispatcher: new Agent({
connect: {
rejectUnauthorized: false
}
})
};
// Add data to body when object is given
if(data !== null) {
request.body = JSON.stringify(data);
}
fetch(`https://${controller.ip}:${controller.port}/proxy/network/integration/v1/sites/${controller.siteUUID}${endpoint}?${querystring.stringify(params)}`, request)
.then((response) => {
return response.json();
})
.then((response) => {
if(response.error) {
log.error(`[UniFi] Error while processing request. Error: ${response.error.message}`);
log.debug(response.error);
reject(response.error.message);
return;
}
if(response.statusCode) {
log.error(`[UniFi] Error while processing request. Error: ${response.message}`);
log.debug(response);
reject(response.message);
return;
}
if(response.data) {
resolve(response.data);
} else {
resolve(response);
}
})
.catch((err) => {
log.error(`[UniFi] Error while processing request. Error: ${err}`);
log.debug(err);
reject(err.message);
});
});
}

View File

@@ -5,7 +5,7 @@
* @returns {*} * @returns {*}
*/ */
module.exports = (string) => { module.exports = (string) => {
if(string === null) { if(string === null || typeof string === 'undefined') {
return { return {
note: null, note: null,
source: null, source: null,

View File

@@ -9,15 +9,15 @@ const variables = require('../modules/variables');
module.exports = (voucher) => { module.exports = (voucher) => {
let base = variables.unifiSsid !== '' ? variables.unifiSsidPassword !== '' ? 415 : 375 : 260; let base = variables.unifiSsid !== '' ? variables.unifiSsidPassword !== '' ? 415 : 375 : 260;
if(voucher.qos_usage_quota) { if(voucher.dataUsageLimitMBytes) {
base += 10; base += 10;
} }
if(voucher.qos_rate_max_down) { if(voucher.rxRateLimitKbps) {
base += 10; base += 10;
} }
if(voucher.qos_rate_max_up) { if(voucher.txRateLimitKbps) {
base += 10; base += 10;
} }