mirror of
https://github.com/glenndehaan/unifi-voucher-site.git
synced 2026-03-31 06:24:00 -04:00
Merge pull request #92 from glenndehaan/feature/unifi-integration-api
Feature/unifi integration api
This commit is contained in:
BIN
.docs/images/integrations_example.png
Normal file
BIN
.docs/images/integrations_example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
75
README.md
75
README.md
@@ -6,7 +6,7 @@ UniFi Voucher Site is a web-based platform for generating and managing UniFi net
|
||||
|
||||

|
||||
|
||||
> Upgrading from 6.x to 7.x? Please take a look at the [migration guide](#migration-from-6x-to-7x)
|
||||
> Upgrading from 7.x to 8.x? Please take a look at the [migration guide](#migration-from-7x-to-8x)
|
||||
|
||||
---
|
||||
|
||||
@@ -41,18 +41,17 @@ UniFi Voucher Site is a web-based platform for generating and managing UniFi net
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- UniFi Network Controller (Cloud Key, Dream Machine, or Controller software)
|
||||
- UniFi OS v4.2.8+
|
||||
- UniFi Network v9.1.119+ (Cloud Gateways, Cloud Key, or UniFi OS software)
|
||||
- UniFi Access Point (AP)
|
||||
- UniFi Local Account with 'Full Management' access
|
||||
- UniFi Integration API Key
|
||||
|
||||

|
||||
|
||||
[Follow this guide to set up the Hotspot Portal](https://help.ui.com/hc/en-us/articles/115000166827-UniFi-Hotspot-Portal-and-Guest-WiFi), then continue with the installation below
|
||||
|
||||
> Ensure voucher authentication is enabled within the Hotspot Portal
|
||||
|
||||
> Attention!: We recommend only using Local UniFi accounts due to short token lengths provided by UniFi Cloud Accounts. Also, UniFi Cloud Accounts using 2FA are not supported!
|
||||
|
||||
> Note: When creating a Local UniFi account ensure you give 'Full Management' access rights to the Network controller. The 'Hotspot Role' won't give access to the API and therefore the application will throw errors.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
@@ -77,10 +76,8 @@ services:
|
||||
UNIFI_IP: '192.168.1.1'
|
||||
# The port of your UniFi OS Console, this could be 443 or 8443
|
||||
UNIFI_PORT: 443
|
||||
# The username of a local UniFi OS account
|
||||
UNIFI_USERNAME: 'admin'
|
||||
# The password of a local UniFi OS account
|
||||
UNIFI_PASSWORD: 'password'
|
||||
# The API Key created on the integrations tab within UniFi OS
|
||||
UNIFI_TOKEN: ''
|
||||
# The UniFi Site ID
|
||||
UNIFI_SITE_ID: 'default'
|
||||
# The UniFi SSID where guests need to connect to (Used within templating and 'Scan to Connect')
|
||||
@@ -142,12 +139,20 @@ services:
|
||||
KIOSK_NAME_REQUIRED: 'false'
|
||||
# Enable/disable a printer for Kiosk Vouchers (this automatically prints vouchers), currently supported: escpos ip (Example: 192.168.1.10)
|
||||
KIOSK_PRINTER: ''
|
||||
# Enable/disable an override to redirect to the Kiosk on the / url (Also enables a link from the Kiosk back to the Admin UI)
|
||||
KIOSK_HOMEPAGE: 'false'
|
||||
# Sets the application Log Level (Valid Options: error|warn|info|debug|trace)
|
||||
LOG_LEVEL: 'info'
|
||||
# Sets the default translation for dropdowns
|
||||
TRANSLATION_DEFAULT: 'en'
|
||||
# Hides languages from the UI, example: en,nl,de
|
||||
TRANSLATION_HIDDEN_LANGUAGES: ''
|
||||
# Enables/disables translation debugging, when enabled only translation keys are shown
|
||||
TRANSLATION_DEBUG: 'false'
|
||||
# Enables/disables an automated task to clean up expired vouchers from UniFi
|
||||
TASK_CLEANUP_EXPIRED: 'false'
|
||||
# Enables/disables an automated task to clean up unused vouchers (Vouchers unused a day after creation) from UniFi
|
||||
TASK_CLEANUP_UNUSED: 'false'
|
||||
# Optional volume mapping to override assets
|
||||
volumes:
|
||||
- ./branding:/kiosk
|
||||
@@ -171,8 +176,7 @@ The structure of the file should use lowercase versions of the environment varia
|
||||
{
|
||||
"unifi_ip": "192.168.1.1",
|
||||
"unifi_port": 443,
|
||||
"unifi_username": "admin",
|
||||
"unifi_password": "password",
|
||||
"unifi_token": "",
|
||||
"unifi_site_id": "default",
|
||||
"unifi_ssid": "",
|
||||
"unifi_ssid_password": "",
|
||||
@@ -201,9 +205,13 @@ The structure of the file should use lowercase versions of the environment varia
|
||||
"kiosk_voucher_types": "480,1,,,;",
|
||||
"kiosk_name_required": false,
|
||||
"kiosk_printer": "",
|
||||
"kiosk_homepage": false,
|
||||
"log_level": "info",
|
||||
"translation_default": "en",
|
||||
"translation_debug": false
|
||||
"translation_hidden_languages": "",
|
||||
"translation_debug": false,
|
||||
"task_cleanup_expired": false,
|
||||
"task_cleanup_unused": false
|
||||
}
|
||||
|
||||
```
|
||||
@@ -714,6 +722,10 @@ KIOSK_VOUCHER_TYPES: '480,1,,,;'
|
||||
KIOSK_PRINTER=192.168.1.50
|
||||
```
|
||||
|
||||
- **`KIOSK_HOMEPAGE`**:
|
||||
- Set to `'true'` to redirect from `/` to `/kiosk` (Instead of the Admin UI).
|
||||
- Set to `'false'` to disable the redirect functionality.
|
||||
|
||||
### Custom Branding (Logo and Background)
|
||||
|
||||
You can customize the appearance of the kiosk page by providing your own `logo.png` and `bg.jpg` images.
|
||||
@@ -824,6 +836,41 @@ Detailed information on the changes in each release can be found on the [GitHub
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migration from 7.x to 8.x
|
||||
|
||||
> **Warning!** This release is only compatible with:
|
||||
> - UniFi OS v4.2.8+
|
||||
> - UniFi Network v9.1.119+ (Cloud Gateways, Cloud Key, or UniFi OS software)
|
||||
|
||||
> **This release requires the setup of a UniFi OS Integration API Key**
|
||||
|
||||
> **Note**: This release breaks the Connected Guests feature due to a limitation within the UniFi Integration API. Please check and upvote this issue: https://community.ui.com/questions/Feature-Request-Network-API-Guest-Access-Voucher-ID/d3c470e2-433d-4386-8a13-211712311202
|
||||
|
||||
When upgrading from 7.x to 8.x, the following changes need to be made:
|
||||
|
||||
1. **UniFi Authentication Changes**
|
||||
|
||||
* The environment variables **`UNIFI_USERNAME`** and **`UNIFI_PASSWORD`** have been **removed** in 8.x.
|
||||
* Authentication is now handled via a single **API token**.
|
||||
|
||||
**Before (7.x):**
|
||||
|
||||
```env
|
||||
UNIFI_USERNAME='admin'
|
||||
UNIFI_PASSWORD='supersecret'
|
||||
```
|
||||
|
||||
**After (8.x):**
|
||||
|
||||
```env
|
||||
UNIFI_TOKEN='your-unifi-api-token'
|
||||
```
|
||||
|
||||
* Generate a new Integration API key within your UniFi Controller’s settings.
|
||||
* Update your environment configuration to use `UNIFI_TOKEN` and remove the old `UNIFI_USERNAME` and `UNIFI_PASSWORD` variables.
|
||||
|
||||

|
||||
|
||||
### Migration from 6.x to 7.x
|
||||
|
||||
When upgrading from 6.x to 7.x, the following changes need to be made:
|
||||
|
||||
247
controllers/api.js
Normal file
247
controllers/api.js
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
const cache = require('../modules/cache');
|
||||
const unifi = require('../modules/unifi');
|
||||
const mail = require('../modules/mail');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const {updateCache} = require('../utils/cache');
|
||||
const types = require('../utils/types');
|
||||
const languages = require('../utils/languages');
|
||||
|
||||
module.exports = {
|
||||
api: {
|
||||
/**
|
||||
* GET - /api
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/types'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/languages'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/vouchers'
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
endpoint: '/api/voucher'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
types: {
|
||||
/**
|
||||
* GET - /api/types
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
types: types(variables.voucherTypes)
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
languages: {
|
||||
/**
|
||||
* GET - /api/languages
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
languages: Object.keys(languages).map(language => {
|
||||
return {
|
||||
code: language,
|
||||
name: languages[language]
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
vouchers: {
|
||||
/**
|
||||
* GET - /api/vouchers
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: async (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
vouchers: cache.vouchers.map((voucher) => {
|
||||
return {
|
||||
id: voucher.id,
|
||||
code: `${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`,
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
voucher: {
|
||||
/**
|
||||
* POST - /api/voucher
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
post: async (req, res) => {
|
||||
// Verify valid body is sent
|
||||
if(!req.body || !req.body.type) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid Body!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if email body is set
|
||||
if(req.body.email) {
|
||||
// Check if email module is enabled
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(400).json({
|
||||
error: 'Email Not Configured!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if email body is correct
|
||||
if(!req.body.email.language || !req.body.email.address) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid Body!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if language is available
|
||||
if(!Object.keys(languages).includes(req.body.email.language)) {
|
||||
res.status(400).json({
|
||||
error: 'Unknown Language!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if type is implemented and valid
|
||||
const typeCheck = (variables.voucherTypes).split(';').includes(req.body.type);
|
||||
if(!typeCheck) {
|
||||
res.status(400).json({
|
||||
error: 'Unknown Type!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.body.type, true), 1, `||;;||api||;;||local||;;||`).catch((e) => {
|
||||
res.status(500).json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
// Update application cache
|
||||
await updateCache();
|
||||
|
||||
if(voucherCode) {
|
||||
// Locate voucher data within cache
|
||||
const voucherData = cache.vouchers.find(voucher => voucher.code === voucherCode.replaceAll('-', ''));
|
||||
if(!voucherData) {
|
||||
res.status(500).json({
|
||||
error: 'Invalid application cache!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we should send and email
|
||||
if(req.body.email) {
|
||||
// Send mail
|
||||
const emailResult = await mail.send(req.body.email.address, voucherData, req.body.email.language).catch((e) => {
|
||||
res.status(500).json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
// Verify is the email was sent successfully
|
||||
if(emailResult) {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: {
|
||||
id: voucherData.id,
|
||||
code: voucherCode
|
||||
},
|
||||
email: {
|
||||
status: 'SENT',
|
||||
address: req.body.email.address
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: {
|
||||
id: voucherData.id,
|
||||
code: voucherCode
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
85
controllers/authentication.js
Normal file
85
controllers/authentication.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
const jwt = require('../modules/jwt');
|
||||
|
||||
module.exports = {
|
||||
login: {
|
||||
/**
|
||||
* GET - /login
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
// Check if authentication is disabled
|
||||
if (variables.authDisabled) {
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hour = new Date().getHours();
|
||||
const timeHeader = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
|
||||
|
||||
res.render('login', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
app_header: timeHeader,
|
||||
internalAuth: variables.authInternalEnabled,
|
||||
oidcAuth: variables.authOidcEnabled
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* POST - /login
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
post: async (req, res) => {
|
||||
// Check if internal authentication is enabled
|
||||
if(!variables.authInternalEnabled) {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordCheck = req.body.password === variables.authInternalPassword;
|
||||
|
||||
if (!passwordCheck) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Password Invalid!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/login`);
|
||||
return;
|
||||
}
|
||||
|
||||
res.cookie('authorization', jwt.sign(), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
},
|
||||
|
||||
logout: {
|
||||
/**
|
||||
* GET - /logout
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
// Check if authentication is disabled
|
||||
if (variables.authDisabled) {
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(req.oidc) {
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/oidc/logout`);
|
||||
} else {
|
||||
res.cookie('authorization', '', {httpOnly: true, expires: new Date(0)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
111
controllers/bulk.js
Normal file
111
controllers/bulk.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
const cache = require('../modules/cache');
|
||||
const print = require('../modules/print');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const notes = require('../utils/notes');
|
||||
const time = require('../utils/time');
|
||||
const bytes = require('../utils/bytes');
|
||||
const languages = require('../utils/languages');
|
||||
|
||||
module.exports = {
|
||||
print: {
|
||||
/**
|
||||
* GET - /bulk/print
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
res.render('components/bulk-print', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
notesConvert: notes,
|
||||
languages,
|
||||
defaultLanguage: variables.translationDefault,
|
||||
printers: variables.printers.split(','),
|
||||
vouchers: cache.vouchers,
|
||||
updated: cache.updated
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* POST - /bulk/print
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
post: async (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!variables.printers.includes(req.body.printer)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!req.body.vouchers) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'No selected vouchers to print!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single checkboxes get send as string so conversion is needed
|
||||
if(typeof req.body.vouchers === 'string') {
|
||||
req.body.vouchers = [req.body.vouchers];
|
||||
}
|
||||
|
||||
const vouchers = req.body.vouchers.map((voucher) => {
|
||||
return cache.vouchers.find((e) => {
|
||||
return e.id === voucher;
|
||||
});
|
||||
});
|
||||
|
||||
if(!vouchers.includes(undefined)) {
|
||||
if(req.body.printer === 'pdf') {
|
||||
const buffers = await print.pdf(vouchers, req.body.language, true);
|
||||
const pdfData = Buffer.concat(buffers);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': Buffer.byteLength(pdfData),
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment;filename=bulk_vouchers_${new Date().getTime()}.pdf`
|
||||
}).end(pdfData);
|
||||
} else {
|
||||
let printSuccess = true;
|
||||
|
||||
for(let voucher = 0; voucher < vouchers.length; voucher++) {
|
||||
const printResult = await print.escpos(vouchers[voucher], req.body.language, req.body.printer).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(!printResult) {
|
||||
printSuccess = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(printSuccess) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: `Vouchers send to printer!`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
36
controllers/error.js
Normal file
36
controllers/error.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const log = require('../modules/log');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Handler for 404 status codes
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
404: (req, res) => {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for 500 status codes
|
||||
*
|
||||
* @param err
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
500: (err, req, res, next) => {
|
||||
log.error(err.stack);
|
||||
res.status(500);
|
||||
res.render('500', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: err.stack
|
||||
});
|
||||
}
|
||||
};
|
||||
168
controllers/kiosk.js
Normal file
168
controllers/kiosk.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
const log = require('../modules/log');
|
||||
const cache = require('../modules/cache');
|
||||
const unifi = require('../modules/unifi');
|
||||
const print = require('../modules/print');
|
||||
const mail = require('../modules/mail');
|
||||
const qr = require('../modules/qr');
|
||||
const translation = require('../modules/translation');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const types = require('../utils/types');
|
||||
const time = require('../utils/time');
|
||||
const bytes = require('../utils/bytes');
|
||||
const languages = require('../utils/languages');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* GET - /kiosk
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
// Check if kiosk is disabled
|
||||
if(!variables.kioskEnabled) {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
res.render('kiosk', {
|
||||
t: translation('kiosk', req.locale.language),
|
||||
languages,
|
||||
language: req.locale.language,
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
voucher_types: types(variables.kioskVoucherTypes),
|
||||
kiosk_name_required: variables.kioskNameRequired,
|
||||
kiosk_homepage: variables.kioskHomepage
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* POST - /kiosk
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
post: async (req, res) => {
|
||||
// Check if kiosk is disabled
|
||||
if(!variables.kioskEnabled) {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we need to generate a voucher or send an email with an existing voucher
|
||||
if(req.body && req.body.id && req.body.code && req.body.email) {
|
||||
// Check if email functions are enabled
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get voucher from cache
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e.id === req.body.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
const emailResult = await mail.send(req.body.email, voucher, req.locale.language).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
});
|
||||
|
||||
if(emailResult) {
|
||||
res.render('kiosk', {
|
||||
t: translation('kiosk', req.locale.language),
|
||||
languages,
|
||||
language: req.locale.language,
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
|
||||
unifiSsid: variables.unifiSsid,
|
||||
unifiSsidPassword: variables.unifiSsidPassword,
|
||||
qr: await qr(),
|
||||
voucherId: req.body.id,
|
||||
voucherCode: req.body.code,
|
||||
email: req.body.email
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const typeCheck = (variables.kioskVoucherTypes).split(';').includes(req.body['voucher-type']);
|
||||
|
||||
if (!typeCheck) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Unknown Type!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(variables.kioskNameRequired && req.body['voucher-note'] !== '' && req.body['voucher-note'].includes('||;;||')) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid Notes!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
return;
|
||||
}
|
||||
|
||||
const voucherNote = `${variables.kioskNameRequired ? req.body['voucher-note'] : ''}||;;||kiosk||;;||local||;;||`;
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.body['voucher-type'], true), 1, voucherNote).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
});
|
||||
|
||||
if (voucherCode) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
});
|
||||
|
||||
if (vouchers) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
|
||||
// Locate voucher data within cache
|
||||
const voucherData = cache.vouchers.find(voucher => voucher.code === voucherCode.replaceAll('-', ''));
|
||||
if(!voucherData) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid application cache!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto print voucher if enabled
|
||||
await print.escpos(voucherData, req.locale.language, variables.kioskPrinter).catch((e) => {
|
||||
log.error(`[Kiosk] Unable to auto-print voucher on printer: ${variables.kioskPrinter}!`);
|
||||
log.error(e);
|
||||
});
|
||||
|
||||
res.render('kiosk', {
|
||||
t: translation('kiosk', req.locale.language),
|
||||
languages,
|
||||
language: req.locale.language,
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
|
||||
unifiSsid: variables.unifiSsid,
|
||||
unifiSsidPassword: variables.unifiSsidPassword,
|
||||
qr: await qr(),
|
||||
voucherId: voucherData.id,
|
||||
voucherCode
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
37
controllers/status.js
Normal file
37
controllers/status.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Import base packages
|
||||
*/
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const status = require('../utils/status');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* GET - /status
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: async (req, res) => {
|
||||
const user = req.oidc ? await req.oidc.fetchUserInfo() : { email: 'admin' };
|
||||
|
||||
res.render('status', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
gitTag: variables.gitTag,
|
||||
gitBuild: variables.gitBuild,
|
||||
kioskEnabled: variables.kioskEnabled,
|
||||
user: user,
|
||||
userIcon: req.oidc ? crypto.createHash('sha256').update(user.email).digest('hex') : '',
|
||||
authDisabled: variables.authDisabled,
|
||||
status: status()
|
||||
});
|
||||
}
|
||||
};
|
||||
291
controllers/voucher.js
Normal file
291
controllers/voucher.js
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
const log = require('../modules/log');
|
||||
const cache = require('../modules/cache');
|
||||
const unifi = require('../modules/unifi');
|
||||
const print = require('../modules/print');
|
||||
const mail = require('../modules/mail');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const types = require('../utils/types');
|
||||
const notes = require('../utils/notes');
|
||||
const time = require('../utils/time');
|
||||
const bytes = require('../utils/bytes');
|
||||
const languages = require('../utils/languages');
|
||||
|
||||
module.exports = {
|
||||
voucher: {
|
||||
/**
|
||||
* GET - /voucher/:id
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e.id === req.params.id;
|
||||
});
|
||||
const guests = cache.guests.filter((e) => {
|
||||
return e.voucher_code === voucher.code;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
res.render('components/details', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
notesConvert: notes,
|
||||
voucher,
|
||||
guests,
|
||||
updated: cache.updated
|
||||
});
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST - /voucher
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
post: async (req, res) => {
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(req.body['voucher-type'] !== 'custom') {
|
||||
const typeCheck = (variables.voucherTypes).split(';').includes(req.body['voucher-type']);
|
||||
|
||||
if (!typeCheck) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Unknown Type!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(req.body['voucher-note'] !== '' && req.body['voucher-note'].includes('||;;||')) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid Notes!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = req.oidc ? await req.oidc.fetchUserInfo() : { email: null };
|
||||
const voucherNote = `${req.body['voucher-note'] !== '' ? req.body['voucher-note'] : ''}||;;||web||;;||${req.oidc ? 'oidc' : 'local'}||;;||${req.oidc ? user.email.split('@')[1].toLowerCase() : ''}`;
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.body['voucher-type'] === 'custom' ? `${req.body['voucher-duration-type'] === 'day' ? (parseInt(req.body['voucher-duration']) * 24 * 60) : req.body['voucher-duration-type'] === 'hour' ? (parseInt(req.body['voucher-duration']) * 60) : parseInt(req.body['voucher-duration'])},${req.body['voucher-usage'] === '-1' ? req.body['voucher-quota'] : req.body['voucher-usage']},${req.body['voucher-upload-limit']},${req.body['voucher-download-limit']},${req.body['voucher-data-limit']};` : req.body['voucher-type'], true), parseInt(req.body['voucher-amount']), voucherNote).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(voucherCode) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(vouchers) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: parseInt(req.body['voucher-amount']) > 1 ? `${req.body['voucher-amount']} Vouchers Created!` : `Voucher Created: ${voucherCode}`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
remove: {
|
||||
/**
|
||||
* GET - /voucher/:id/remove
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: async (req, res) => {
|
||||
// Revoke voucher code
|
||||
const response = await unifi.remove(req.params.id).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(response) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(vouchers) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: `Voucher Removed!`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
print: {
|
||||
/**
|
||||
* GET - /voucher/:id/print
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e.id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
res.render('components/print', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
languages,
|
||||
defaultLanguage: variables.translationDefault,
|
||||
printers: variables.printers.split(','),
|
||||
voucher,
|
||||
updated: cache.updated
|
||||
});
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST - /voucher/:id/print
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
post: async (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!variables.printers.includes(req.body.printer)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e.id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
if(req.body.printer === 'pdf') {
|
||||
const buffers = await print.pdf(voucher, req.body.language);
|
||||
const pdfData = Buffer.concat(buffers);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': Buffer.byteLength(pdfData),
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment;filename=voucher_${req.params.id}.pdf`
|
||||
}).end(pdfData);
|
||||
} else {
|
||||
const printResult = await print.escpos(voucher, req.body.language, req.body.printer).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(printResult) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: `Voucher send to printer!`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
email: {
|
||||
/**
|
||||
* GET - /voucher/:id/email
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: (req, res) => {
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e.id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
res.render('components/email', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
languages,
|
||||
defaultLanguage: variables.translationDefault,
|
||||
voucher,
|
||||
updated: cache.updated
|
||||
});
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST - /voucher/:id/email
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
post: async (req, res) => {
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e.id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
const emailResult = await mail.send(req.body.email, voucher, req.body.language).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(emailResult) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: 'Email has been sent!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
142
controllers/vouchers.js
Normal file
142
controllers/vouchers.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Import base packages
|
||||
*/
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
const log = require('../modules/log');
|
||||
const cache = require('../modules/cache');
|
||||
const unifi = require('../modules/unifi');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const types = require('../utils/types');
|
||||
const notes = require('../utils/notes');
|
||||
const time = require('../utils/time');
|
||||
const bytes = require('../utils/bytes');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* GET - /vouchers
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
get: async (req, res) => {
|
||||
if(req.query.refresh) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(!vouchers) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('[Cache] Requesting UniFi Guests...');
|
||||
|
||||
const guests = await unifi.guests().catch((e) => {
|
||||
log.error('[Cache] Error requesting guests!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(vouchers && guests) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.guests = guests;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
log.info(`[Cache] Saved ${guests.length} guest(s)`);
|
||||
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: 'Synced Vouchers & Guests!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const user = req.oidc ? await req.oidc.fetchUserInfo() : { email: 'admin' };
|
||||
|
||||
res.render('voucher', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
gitTag: variables.gitTag,
|
||||
gitBuild: variables.gitBuild,
|
||||
user: user,
|
||||
userIcon: req.oidc ? crypto.createHash('sha256').update(user.email).digest('hex') : '',
|
||||
authDisabled: variables.authDisabled,
|
||||
info: req.flashMessage.type === 'info',
|
||||
info_text: req.flashMessage.message || '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
kioskEnabled: variables.kioskEnabled,
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
notesConvert: notes,
|
||||
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
|
||||
printer_enabled: variables.printers !== '',
|
||||
voucher_types: types(variables.voucherTypes),
|
||||
voucher_custom: variables.voucherCustom,
|
||||
vouchers: cache.vouchers.filter((item) => {
|
||||
if(variables.authOidcRestrictVisibility && req.oidc) {
|
||||
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.authorizedGuestCount === 0 && !item.expired;
|
||||
}
|
||||
|
||||
if(req.query.status === 'in-use') {
|
||||
return item.authorizedGuestCount > 0 && !item.expired;
|
||||
}
|
||||
|
||||
if(req.query.status === 'expired') {
|
||||
return item.expired;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).filter((item) => {
|
||||
if(req.query.quota === 'multi-use') {
|
||||
return (item.authorizedGuestLimit && item.authorizedGuestLimit > 1) || !item.authorizedGuestLimit;
|
||||
}
|
||||
|
||||
if(req.query.quota === 'single-use') {
|
||||
return item.authorizedGuestLimit && item.authorizedGuestLimit === 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).sort((a, b) => {
|
||||
if(req.query.sort === 'code') {
|
||||
if (a.code > b.code) return -1;
|
||||
if (a.code < b.code) return 1;
|
||||
}
|
||||
|
||||
if(req.query.sort === 'note') {
|
||||
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.timeLimitMinutes > b.timeLimitMinutes) return -1;
|
||||
if (a.timeLimitMinutes < b.timeLimitMinutes) return 1;
|
||||
}
|
||||
|
||||
if(req.query.sort === 'status') {
|
||||
if (a.authorizedGuestCount > b.authorizedGuestCount) return -1;
|
||||
if (a.authorizedGuestCount < b.authorizedGuestCount) return 1;
|
||||
}
|
||||
}),
|
||||
updated: cache.updated,
|
||||
filters: {
|
||||
status: req.query.status,
|
||||
quota: req.query.quota
|
||||
},
|
||||
sort: req.query.sort
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -6,8 +6,7 @@ services:
|
||||
environment:
|
||||
UNIFI_IP: '192.168.1.1'
|
||||
UNIFI_PORT: 443
|
||||
UNIFI_USERNAME: 'admin'
|
||||
UNIFI_PASSWORD: 'password'
|
||||
UNIFI_TOKEN: ''
|
||||
UNIFI_SITE_ID: 'default'
|
||||
UNIFI_SSID: ''
|
||||
UNIFI_SSID_PASSWORD: ''
|
||||
@@ -36,5 +35,10 @@ services:
|
||||
KIOSK_VOUCHER_TYPES: '480,1,,,;'
|
||||
KIOSK_NAME_REQUIRED: 'false'
|
||||
KIOSK_PRINTER: ''
|
||||
KIOSK_HOMEPAGE: 'false'
|
||||
LOG_LEVEL: 'info'
|
||||
TRANSLATION_DEFAULT: 'en'
|
||||
TRANSLATION_HIDDEN_LANGUAGES: ''
|
||||
TRANSLATION_DEBUG: 'false'
|
||||
TASK_CLEANUP_EXPIRED: 'false'
|
||||
TASK_CLEANUP_UNUSED: 'false'
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/**
|
||||
* Internal application cache
|
||||
*
|
||||
* @type {{guests: *[], vouchers: *[], updated: number}}
|
||||
* @type {{unifi: {siteUUID: null}, vouchers: *[], guests: *[], updated: number}}
|
||||
*/
|
||||
module.exports = {
|
||||
unifi: {
|
||||
siteUUID: null
|
||||
},
|
||||
vouchers: [],
|
||||
guests: [],
|
||||
updated: 0
|
||||
|
||||
@@ -7,6 +7,7 @@ const fs = require('fs');
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('./variables');
|
||||
const config = require('./config');
|
||||
const log = require('./log');
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,10 @@ module.exports = () => {
|
||||
if(typeof process.env[item] !== 'undefined') {
|
||||
log.warn(`[Deprecation] '${item}' has been deprecated! Please remove this item from the environment variables and/or follow migration guides: https://github.com/glenndehaan/unifi-voucher-site#migration-guide`);
|
||||
}
|
||||
|
||||
if(config(item.toLowerCase()) !== null) {
|
||||
log.warn(`[Deprecation] '${item.toLowerCase()}' has been deprecated! Please remove this item from the options file and/or follow migration guides: https://github.com/glenndehaan/unifi-voucher-site#migration-guide`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -92,6 +97,11 @@ module.exports = () => {
|
||||
log.error(`[OIDC] Incorrect Configuration Detected!. Verify 'AUTH_OIDC_ISSUER_BASE_URL', 'AUTH_OIDC_APP_BASE_URL', 'AUTH_OIDC_CLIENT_ID' and 'AUTH_OIDC_CLIENT_SECRET' are set! Authentication will be unstable or disabled until issue is resolved!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log translation status
|
||||
*/
|
||||
log.info(`[Translation] Default Language: ${variables.translationDefault}${variables.translationDebug ? ', Debugger: Enabled' : ', Debugger: Disabled'}${variables.translationHiddenLanguages !== '' ? `, Hidden Languages: ${variables.translationHiddenLanguages}` : ''}`)
|
||||
|
||||
/**
|
||||
* Log printer status
|
||||
*/
|
||||
@@ -125,9 +135,15 @@ module.exports = () => {
|
||||
log.info(`[UniFi] Using Controller on: ${variables.unifiIp}:${variables.unifiPort} (Site ID: ${variables.unifiSiteId}${variables.unifiSsid !== '' ? `, SSID: ${variables.unifiSsid}` : ''})`);
|
||||
|
||||
/**
|
||||
* Check for valid UniFi username
|
||||
* Check if UniFi Token is set
|
||||
*/
|
||||
if(variables.unifiUsername.includes('@')) {
|
||||
log.error('[UniFi] Incorrect username detected! UniFi Cloud credentials are not supported!');
|
||||
if(variables.unifiToken === '') {
|
||||
log.error('[UniFi] Integration API Key is not set within UNIFI_TOKEN environment variable!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary warning that guests lookup feature is unavailable
|
||||
*/
|
||||
log.warn('[UniFi] Connected Guests features are temporary disabled in this version of UniFi Voucher Site (Not supported in current Integrations API). Please view and upvote: https://community.ui.com/questions/Feature-Request-Network-API-Guest-Access-Voucher-ID/d3c470e2-433d-4386-8a13-211712311202')
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
293
modules/unifi.js
293
modules/unifi.js
@@ -1,133 +1,65 @@
|
||||
/**
|
||||
* Import vendor modules
|
||||
*/
|
||||
const unifi = require('node-unifi');
|
||||
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('./variables');
|
||||
const log = require('./log');
|
||||
|
||||
/**
|
||||
* UniFi Settings
|
||||
*/
|
||||
const settings = {
|
||||
ip: variables.unifiIp,
|
||||
port: variables.unifiPort,
|
||||
username: variables.unifiUsername,
|
||||
password: variables.unifiPassword,
|
||||
siteID: variables.unifiSiteId
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller session
|
||||
*/
|
||||
let controller = null;
|
||||
|
||||
/**
|
||||
* Start a UniFi controller reusable session
|
||||
*
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
const startSession = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if we have a current session already
|
||||
if(controller !== null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if(settings.username.includes('@')) {
|
||||
reject('[UniFi] Incorrect username detected! UniFi Cloud credentials are not supported!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new UniFi controller object
|
||||
controller = new unifi.Controller({
|
||||
host: settings.ip,
|
||||
port: settings.port,
|
||||
site: settings.siteID,
|
||||
sslverify: false
|
||||
});
|
||||
|
||||
// Login to UniFi Controller
|
||||
controller.login(settings.username, settings.password).then(() => {
|
||||
log.debug('[UniFi] Login successful!');
|
||||
resolve();
|
||||
}).catch((e) => {
|
||||
// Something went wrong so clear the current controller so a user can retry
|
||||
controller = null;
|
||||
log.error('[UniFi] Error while logging in!');
|
||||
log.debug(e);
|
||||
reject('[UniFi] Error while logging in!');
|
||||
});
|
||||
});
|
||||
}
|
||||
const fetch = require('../utils/fetch');
|
||||
|
||||
/**
|
||||
* 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(): Promise<*>)}}
|
||||
*/
|
||||
const unifiModule = {
|
||||
module.exports = {
|
||||
/**
|
||||
* Creates a new UniFi Voucher
|
||||
*
|
||||
* @param type
|
||||
* @param amount
|
||||
* @param note
|
||||
* @param retry
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
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 +68,17 @@ const unifiModule = {
|
||||
* Removes a UniFi Voucher
|
||||
*
|
||||
* @param id
|
||||
* @param retry
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
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 +86,22 @@ const unifiModule = {
|
||||
/**
|
||||
* Returns a list with all UniFi Vouchers
|
||||
*
|
||||
* @param retry
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
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!');
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -227,51 +109,24 @@ const unifiModule = {
|
||||
/**
|
||||
* Returns a list with all UniFi Guests
|
||||
*
|
||||
* @param retry
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
guests: (retry = true) => {
|
||||
guests: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
startSession().then(() => {
|
||||
controller.getGuests().then((guests) => {
|
||||
log.info(`[UniFi] Found ${guests.length} guest(s)`);
|
||||
resolve(guests);
|
||||
}).catch((e) => {
|
||||
log.error('[UniFi] Error while getting guests!');
|
||||
log.debug(e);
|
||||
// fetch('/clients', 'GET', {
|
||||
// filter: 'access.type.eq(\'GUEST\')',
|
||||
// limit: 10000
|
||||
// }).then((clients) => {
|
||||
// console.log(clients);
|
||||
// log.info(`[UniFi] Found ${clients.length} guest(s)`);
|
||||
// }).catch((e) => {
|
||||
// log.error('[UniFi] Error while getting guests!');
|
||||
// log.debug(e);
|
||||
// reject('[UniFi] Error while getting guests!');
|
||||
// });
|
||||
|
||||
// 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.guests(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 guests!');
|
||||
}
|
||||
} 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 guests!');
|
||||
}
|
||||
});
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
// Currently disabled! Waiting on: https://community.ui.com/questions/Feature-Request-Network-API-Guest-Access-Voucher-ID/d3c470e2-433d-4386-8a13-211712311202
|
||||
resolve([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the UniFi module functions
|
||||
*/
|
||||
module.exports = unifiModule;
|
||||
|
||||
@@ -14,8 +14,7 @@ const config = require('./config');
|
||||
module.exports = {
|
||||
unifiIp: config('unifi_ip') || process.env.UNIFI_IP || '192.168.1.1',
|
||||
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 || '',
|
||||
@@ -44,9 +43,13 @@ module.exports = {
|
||||
kioskVoucherTypes: config('kiosk_voucher_types') || process.env.KIOSK_VOUCHER_TYPES || '480,1,,,;',
|
||||
kioskNameRequired: config('kiosk_name_required') || (process.env.KIOSK_NAME_REQUIRED === 'true') || false,
|
||||
kioskPrinter: config('kiosk_printer') || process.env.KIOSK_PRINTER || '',
|
||||
kioskHomepage: config('kiosk_homepage') || (process.env.KIOSK_HOMEPAGE === 'true') || false,
|
||||
logLevel: config('log_level') || process.env.LOG_LEVEL || 'info',
|
||||
translationDefault: config('translation_default') || process.env.TRANSLATION_DEFAULT || 'en',
|
||||
translationHiddenLanguages: config('translation_hidden_languages') || process.env.TRANSLATION_HIDDEN_LANGUAGES || '',
|
||||
translationDebug: config('translation_debug') || (process.env.TRANSLATION_DEBUG === 'true') || false,
|
||||
taskCleanupExpired: config('task_cleanup_expired') || (process.env.TASK_CLEANUP_EXPIRED === 'true') || false,
|
||||
taskCleanupUnused: config('task_cleanup_unused') || (process.env.TASK_CLEANUP_UNUSED === 'true') || false,
|
||||
gitTag: process.env.GIT_TAG || 'master',
|
||||
gitBuild: fs.existsSync('/etc/unifi_voucher_site_build') ? fs.readFileSync('/etc/unifi_voucher_site_build', 'utf-8') : 'Development'
|
||||
};
|
||||
|
||||
488
package-lock.json
generated
488
package-lock.json
generated
@@ -13,39 +13,25 @@
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"express-locale": "^2.0.2",
|
||||
"express-openid-connect": "^2.18.1",
|
||||
"express-openid-connect": "^2.19.2",
|
||||
"js-logger": "^1.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"node-thermal-printer": "^4.5.0",
|
||||
"node-unifi": "^2.5.1",
|
||||
"nodemailer": "^7.0.5",
|
||||
"pdfkit": "^0.17.1",
|
||||
"qrcode": "^1.5.4"
|
||||
"qrcode": "^1.5.4",
|
||||
"undici": "^7.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.1.11",
|
||||
"@tailwindcss/cli": "^4.1.12",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"tailwindcss": "^4.1.11"
|
||||
"tailwindcss": "^4.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
@@ -75,9 +61,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -85,6 +71,17 @@
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -96,16 +93,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -486,19 +483,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/cli": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.11.tgz",
|
||||
"integrity": "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.12.tgz",
|
||||
"integrity": "sha512-2PyJ5MGh/6JPS+cEaAq6MGDx3UemkX/mJt+/phm7/VOpycpecwNnHuFZbbgx6TNK/aIjvFOhhTVlappM7tmqvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@parcel/watcher": "^2.5.1",
|
||||
"@tailwindcss/node": "4.1.11",
|
||||
"@tailwindcss/oxide": "4.1.11",
|
||||
"enhanced-resolve": "^5.18.1",
|
||||
"@tailwindcss/node": "4.1.12",
|
||||
"@tailwindcss/oxide": "4.1.12",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"mri": "^1.2.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"tailwindcss": "4.1.11"
|
||||
"tailwindcss": "4.1.12"
|
||||
},
|
||||
"bin": {
|
||||
"tailwindcss": "dist/index.mjs"
|
||||
@@ -518,25 +515,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
|
||||
"integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
|
||||
"integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"enhanced-resolve": "^5.18.1",
|
||||
"jiti": "^2.4.2",
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.5.1",
|
||||
"lightningcss": "1.30.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.11"
|
||||
"tailwindcss": "4.1.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
|
||||
"integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
|
||||
"integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -548,24 +545,24 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.11",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.11",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.11",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.11",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.11",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.11",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.11",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.12",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.12",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.12",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.12",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.12",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz",
|
||||
"integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
|
||||
"integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -580,9 +577,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz",
|
||||
"integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
|
||||
"integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -597,9 +594,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz",
|
||||
"integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
|
||||
"integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -614,9 +611,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz",
|
||||
"integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
|
||||
"integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -631,9 +628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz",
|
||||
"integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
|
||||
"integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -648,9 +645,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz",
|
||||
"integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
|
||||
"integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -665,9 +662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz",
|
||||
"integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
|
||||
"integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -682,9 +679,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
|
||||
"integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
|
||||
"integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -699,9 +696,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz",
|
||||
"integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
|
||||
"integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -716,9 +713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz",
|
||||
"integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
|
||||
"integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -734,11 +731,11 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@emnapi/wasi-threads": "^1.0.2",
|
||||
"@napi-rs/wasm-runtime": "^0.2.11",
|
||||
"@tybys/wasm-util": "^0.9.0",
|
||||
"@emnapi/core": "^1.4.5",
|
||||
"@emnapi/runtime": "^1.4.5",
|
||||
"@emnapi/wasi-threads": "^1.0.4",
|
||||
"@napi-rs/wasm-runtime": "^0.2.12",
|
||||
"@tybys/wasm-util": "^0.10.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -746,9 +743,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
|
||||
"integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
|
||||
"integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -763,9 +760,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz",
|
||||
"integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
|
||||
"integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -868,17 +865,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
|
||||
"integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/aggregate-error": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
@@ -916,22 +902,6 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1294,17 +1264,6 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -1385,9 +1344,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1446,14 +1405,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -1549,9 +1500,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.2",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
||||
"integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==",
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1591,21 +1542,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -1621,11 +1557,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter2": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
|
||||
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
@@ -1675,20 +1606,20 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express-openid-connect": {
|
||||
"version": "2.18.1",
|
||||
"resolved": "https://registry.npmjs.org/express-openid-connect/-/express-openid-connect-2.18.1.tgz",
|
||||
"integrity": "sha512-trHqgwXxWF0n/XrDsRzsvQtnBNbU03iCNXbKR/sHwBqXlvCgup341bW7B8t6nr3L/CMoDpK+9gsTnx3qLCqdjQ==",
|
||||
"version": "2.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express-openid-connect/-/express-openid-connect-2.19.2.tgz",
|
||||
"integrity": "sha512-hRRRBS+mH9hrhVcbg7+APe+dIsYB4BDLILv7QfTmM1jSDyaU9NYpTxqWourAnlud/E4Gf4Q0qCVmSJguh4BTaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64url": "^3.0.1",
|
||||
"clone": "^2.1.2",
|
||||
"cookie": "^0.7.1",
|
||||
"debug": "^4.3.4",
|
||||
"futoin-hkdf": "^1.5.1",
|
||||
"cookie": "^0.7.2",
|
||||
"debug": "^4.4.1",
|
||||
"futoin-hkdf": "^1.5.3",
|
||||
"http-errors": "^1.8.1",
|
||||
"joi": "^17.7.0",
|
||||
"joi": "^17.13.3",
|
||||
"jose": "^2.0.7",
|
||||
"on-headers": "^1.0.2",
|
||||
"on-headers": "^1.1.0",
|
||||
"openid-client": "^4.9.1",
|
||||
"url-join": "^4.0.1"
|
||||
},
|
||||
@@ -1864,25 +1795,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fontkit": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
||||
@@ -1900,22 +1812,6 @@
|
||||
"unicode-trie": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -2075,21 +1971,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -2108,33 +1989,6 @@
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/http-cookie-agent": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-5.0.4.tgz",
|
||||
"integrity": "sha512-OtvikW69RvfyP6Lsequ0fN5R49S+8QcS9zwd58k6VSr6r57T8G29BkPdyrBcSwLq6ExLs9V+rBlfxu7gDstJag==",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0 <15.0.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/3846masa"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"deasync": "^0.1.26",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"undici": "^5.11.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"deasync": {
|
||||
"optional": true
|
||||
},
|
||||
"undici": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -2263,9 +2117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -2699,13 +2553,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"version": "0.30.18",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
@@ -2909,22 +2763,6 @@
|
||||
"write-file-queue": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-unifi": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/node-unifi/-/node-unifi-2.5.1.tgz",
|
||||
"integrity": "sha512-mYLJFNKhONaXIFU2PeQ+p1fjr6C3q/Na8XyhZXpGalOArCAJLzpAoWl1rg9ZbmuJiVqwprqCq3u9Srn23CcpuA==",
|
||||
"dependencies": {
|
||||
"axios": "1.6.2",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"http-cookie-agent": "^5.0.4",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"url": "^0.11.3",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0 <15.0.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||
@@ -3171,11 +3009,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@@ -3559,20 +3392,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
|
||||
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
@@ -3624,24 +3461,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.78",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.78.tgz",
|
||||
"integrity": "sha512-fSgYrW0ITH0SR/CqKMXIruYIPpNu5aDgUp22UhYoSrnUQwc7SBqifEBFNce7AAcygUPBo6a/gbtcguWdmko4RQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.78"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.78",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.78.tgz",
|
||||
"integrity": "sha512-jS0svNsB99jR6AJBmfmEWuKIgz91Haya91Z43PATaeHJ24BkMoNRb/jlaD37VYjb0mYf6gRL/HOnvS1zEnYBiw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -3663,18 +3482,6 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -3699,6 +3506,15 @@
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz",
|
||||
"integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
@@ -3742,26 +3558,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/url": {
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz",
|
||||
"integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==",
|
||||
"dependencies": {
|
||||
"punycode": "^1.4.1",
|
||||
"qs": "^6.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/url/node_modules/punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -3797,26 +3599,6 @@
|
||||
"dank-do-while": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
16
package.json
16
package.json
@@ -14,30 +14,24 @@
|
||||
},
|
||||
"author": "Glenn de Haan",
|
||||
"license": "MIT",
|
||||
"overrides": {
|
||||
"node-unifi@^2.5.1": {
|
||||
"axios": "1.11.0",
|
||||
"tough-cookie": "5.1.2"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.7",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"express-locale": "^2.0.2",
|
||||
"express-openid-connect": "^2.18.1",
|
||||
"express-openid-connect": "^2.19.2",
|
||||
"js-logger": "^1.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"node-thermal-printer": "^4.5.0",
|
||||
"node-unifi": "^2.5.1",
|
||||
"nodemailer": "^7.0.5",
|
||||
"pdfkit": "^0.17.1",
|
||||
"qrcode": "^1.5.4"
|
||||
"qrcode": "^1.5.4",
|
||||
"undici": "^7.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.1.11",
|
||||
"@tailwindcss/cli": "^4.1.12",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"tailwindcss": "^4.1.11"
|
||||
"tailwindcss": "^4.1.12"
|
||||
}
|
||||
}
|
||||
|
||||
873
server.js
873
server.js
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const cookieParser = require('cookie-parser');
|
||||
@@ -14,15 +13,9 @@ const locale = require('express-locale');
|
||||
*/
|
||||
const variables = require('./modules/variables');
|
||||
const log = require('./modules/log');
|
||||
const cache = require('./modules/cache');
|
||||
const jwt = require('./modules/jwt');
|
||||
const info = require('./modules/info');
|
||||
const unifi = require('./modules/unifi');
|
||||
const print = require('./modules/print');
|
||||
const mail = require('./modules/mail');
|
||||
const oidc = require('./modules/oidc');
|
||||
const qr = require('./modules/qr');
|
||||
const translation = require('./modules/translation');
|
||||
|
||||
/**
|
||||
* Import own middlewares
|
||||
@@ -30,16 +23,23 @@ const translation = require('./modules/translation');
|
||||
const authorization = require('./middlewares/authorization');
|
||||
const flashMessage = require('./middlewares/flashMessage');
|
||||
|
||||
/**
|
||||
* Import own controllers
|
||||
*/
|
||||
const api = require('./controllers/api');
|
||||
const authentication = require('./controllers/authentication');
|
||||
const bulk = require('./controllers/bulk');
|
||||
const error = require('./controllers/error');
|
||||
const kiosk = require('./controllers/kiosk');
|
||||
const status = require('./controllers/status');
|
||||
const voucher = require('./controllers/voucher');
|
||||
const vouchers = require('./controllers/vouchers');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const {updateCache} = require('./utils/cache');
|
||||
const types = require('./utils/types');
|
||||
const notes = require('./utils/notes');
|
||||
const time = require('./utils/time');
|
||||
const bytes = require('./utils/bytes');
|
||||
const status = require('./utils/status');
|
||||
const languages = require('./utils/languages');
|
||||
const {cleanupExpired, cleanupUnused} = require('./utils/cleanup');
|
||||
|
||||
/**
|
||||
* Setup Express app
|
||||
@@ -138,835 +138,64 @@ app.use(locale({
|
||||
app.use(flashMessage);
|
||||
|
||||
/**
|
||||
* Configure routers
|
||||
* Setup Base Routes
|
||||
*/
|
||||
app.get('/', (req, res) => {
|
||||
if(variables.serviceWeb) {
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/${variables.kioskEnabled && variables.kioskHomepage ? 'kiosk' : 'vouchers'}`);
|
||||
} else {
|
||||
res.status(501).send();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if web service is enabled
|
||||
/**
|
||||
* Setup Web Routes
|
||||
*/
|
||||
if(variables.serviceWeb) {
|
||||
app.get('/kiosk', (req, res) => {
|
||||
// Check if kiosk is disabled
|
||||
if(!variables.kioskEnabled) {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
app.get('/kiosk', kiosk.get);
|
||||
app.post('/kiosk', kiosk.post);
|
||||
|
||||
res.render('kiosk', {
|
||||
t: translation('kiosk', req.locale.language),
|
||||
languages,
|
||||
language: req.locale.language,
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
voucher_types: types(variables.kioskVoucherTypes),
|
||||
kiosk_name_required: variables.kioskNameRequired
|
||||
});
|
||||
});
|
||||
app.post('/kiosk', async (req, res) => {
|
||||
// Check if kiosk is disabled
|
||||
if(!variables.kioskEnabled) {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
app.get('/login', authentication.login.get);
|
||||
app.post('/login', authentication.login.post);
|
||||
app.get('/logout', [authorization.web], authentication.logout.get);
|
||||
|
||||
// Check if we need to generate a voucher or send an email with an existing voucher
|
||||
if(req.body && req.body.id && req.body.code && req.body.email) {
|
||||
// Check if email functions are enabled
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
app.post('/voucher', [authorization.web], voucher.voucher.post);
|
||||
app.get('/voucher/:id/remove', [authorization.web], voucher.remove.get);
|
||||
app.get('/voucher/:id/print', [authorization.web], voucher.print.get);
|
||||
app.post('/voucher/:id/print', [authorization.web], voucher.print.post);
|
||||
app.get('/voucher/:id/email', [authorization.web], voucher.email.get);
|
||||
app.post('/voucher/:id/email', [authorization.web], voucher.email.post);
|
||||
|
||||
// Get voucher from cache
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e._id === req.body.id;
|
||||
});
|
||||
app.get('/vouchers', [authorization.web], vouchers.get);
|
||||
app.get('/voucher/:id', [authorization.web], voucher.voucher.get);
|
||||
|
||||
if(voucher) {
|
||||
const emailResult = await mail.send(req.body.email, voucher, req.locale.language).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
});
|
||||
app.get('/status', [authorization.web], status.get);
|
||||
|
||||
if(emailResult) {
|
||||
res.render('kiosk', {
|
||||
t: translation('kiosk', req.locale.language),
|
||||
languages,
|
||||
language: req.locale.language,
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
|
||||
unifiSsid: variables.unifiSsid,
|
||||
unifiSsidPassword: variables.unifiSsidPassword,
|
||||
qr: await qr(),
|
||||
voucherId: req.body.id,
|
||||
voucherCode: req.body.code,
|
||||
email: req.body.email
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const typeCheck = (variables.kioskVoucherTypes).split(';').includes(req.body['voucher-type']);
|
||||
|
||||
if (!typeCheck) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Unknown Type!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(variables.kioskNameRequired && req.body['voucher-note'] !== '' && req.body['voucher-note'].includes('||;;||')) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid Notes!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
return;
|
||||
}
|
||||
|
||||
const voucherNote = `${variables.kioskNameRequired ? req.body['voucher-note'] : ''}||;;||kiosk||;;||local||;;||`;
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.body['voucher-type'], true), 1, voucherNote).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
});
|
||||
|
||||
if (voucherCode) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
});
|
||||
|
||||
if (vouchers) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
|
||||
// Locate voucher data within cache
|
||||
const voucherData = cache.vouchers.find(voucher => voucher.code === voucherCode.replaceAll('-', ''));
|
||||
if(!voucherData) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid application cache!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/kiosk`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto print voucher if enabled
|
||||
await print.escpos(voucherData, req.locale.language, variables.kioskPrinter).catch((e) => {
|
||||
log.error(`[Kiosk] Unable to auto-print voucher on printer: ${variables.kioskPrinter}!`);
|
||||
log.error(e);
|
||||
});
|
||||
|
||||
res.render('kiosk', {
|
||||
t: translation('kiosk', req.locale.language),
|
||||
languages,
|
||||
language: req.locale.language,
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
|
||||
unifiSsid: variables.unifiSsid,
|
||||
unifiSsidPassword: variables.unifiSsidPassword,
|
||||
qr: await qr(),
|
||||
voucherId: voucherData._id,
|
||||
voucherCode
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
app.get('/login', (req, res) => {
|
||||
// Check if authentication is disabled
|
||||
if (variables.authDisabled) {
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hour = new Date().getHours();
|
||||
const timeHeader = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
|
||||
|
||||
res.render('login', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
app_header: timeHeader,
|
||||
internalAuth: variables.authInternalEnabled,
|
||||
oidcAuth: variables.authOidcEnabled
|
||||
});
|
||||
});
|
||||
app.post('/login', async (req, res) => {
|
||||
// Check if internal authentication is enabled
|
||||
if(!variables.authInternalEnabled) {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordCheck = req.body.password === variables.authInternalPassword;
|
||||
|
||||
if (!passwordCheck) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Password Invalid!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/login`);
|
||||
return;
|
||||
}
|
||||
|
||||
res.cookie('authorization', jwt.sign(), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
app.get('/logout', [authorization.web], (req, res) => {
|
||||
// Check if authentication is disabled
|
||||
if (variables.authDisabled) {
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(req.oidc) {
|
||||
res.redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/oidc/logout`);
|
||||
} else {
|
||||
res.cookie('authorization', '', {httpOnly: true, expires: new Date(0)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/`);
|
||||
}
|
||||
});
|
||||
app.post('/voucher', [authorization.web], async (req, res) => {
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(req.body['voucher-type'] !== 'custom') {
|
||||
const typeCheck = (variables.voucherTypes).split(';').includes(req.body['voucher-type']);
|
||||
|
||||
if (!typeCheck) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Unknown Type!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(req.body['voucher-note'] !== '' && req.body['voucher-note'].includes('||;;||')) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'Invalid Notes!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = req.oidc ? await req.oidc.fetchUserInfo() : { email: null };
|
||||
const voucherNote = `${req.body['voucher-note'] !== '' ? req.body['voucher-note'] : ''}||;;||web||;;||${req.oidc ? 'oidc' : 'local'}||;;||${req.oidc ? user.email.split('@')[1].toLowerCase() : ''}`;
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.body['voucher-type'] === 'custom' ? `${req.body['voucher-duration-type'] === 'day' ? (parseInt(req.body['voucher-duration']) * 24 * 60) : req.body['voucher-duration-type'] === 'hour' ? (parseInt(req.body['voucher-duration']) * 60) : parseInt(req.body['voucher-duration'])},${req.body['voucher-usage'] === '-1' ? req.body['voucher-quota'] : req.body['voucher-usage']},${req.body['voucher-upload-limit']},${req.body['voucher-download-limit']},${req.body['voucher-data-limit']};` : req.body['voucher-type'], true), parseInt(req.body['voucher-amount']), voucherNote).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(voucherCode) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(vouchers) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: parseInt(req.body['voucher-amount']) > 1 ? `${req.body['voucher-amount']} Vouchers Created!` : `Voucher Created: ${voucherCode}`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.get('/voucher/:id/remove', [authorization.web], async (req, res) => {
|
||||
// Revoke voucher code
|
||||
const response = await unifi.remove(req.params.id).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(response) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(vouchers) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: `Voucher Removed!`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.get('/voucher/:id/print', [authorization.web], async (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e._id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
res.render('components/print', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
languages,
|
||||
defaultLanguage: variables.translationDefault,
|
||||
printers: variables.printers.split(','),
|
||||
voucher,
|
||||
updated: cache.updated
|
||||
});
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
});
|
||||
app.post('/voucher/:id/print', [authorization.web], async (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!variables.printers.includes(req.body.printer)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e._id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
if(req.body.printer === 'pdf') {
|
||||
const buffers = await print.pdf(voucher, req.body.language);
|
||||
const pdfData = Buffer.concat(buffers);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': Buffer.byteLength(pdfData),
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment;filename=voucher_${req.params.id}.pdf`
|
||||
}).end(pdfData);
|
||||
} else {
|
||||
const printResult = await print.escpos(voucher, req.body.language, req.body.printer).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(printResult) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: `Voucher send to printer!`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
});
|
||||
app.get('/voucher/:id/email', [authorization.web], async (req, res) => {
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e._id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
res.render('components/email', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
languages,
|
||||
defaultLanguage: variables.translationDefault,
|
||||
voucher,
|
||||
updated: cache.updated
|
||||
});
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
});
|
||||
app.post('/voucher/:id/email', [authorization.web], async (req, res) => {
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e._id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
const emailResult = await mail.send(req.body.email, voucher, req.body.language).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(emailResult) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: 'Email has been sent!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
});
|
||||
app.get('/vouchers', [authorization.web], async (req, res) => {
|
||||
if(req.query.refresh) {
|
||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||
|
||||
const vouchers = await unifi.list().catch((e) => {
|
||||
log.error('[Cache] Error requesting vouchers!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(!vouchers) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('[Cache] Requesting UniFi Guests...');
|
||||
|
||||
const guests = await unifi.guests().catch((e) => {
|
||||
log.error('[Cache] Error requesting guests!');
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(vouchers && guests) {
|
||||
cache.vouchers = vouchers;
|
||||
cache.guests = guests;
|
||||
cache.updated = new Date().getTime();
|
||||
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
|
||||
log.info(`[Cache] Saved ${guests.length} guest(s)`);
|
||||
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: 'Synced Vouchers & Guests!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const user = req.oidc ? await req.oidc.fetchUserInfo() : { email: 'admin' };
|
||||
|
||||
res.render('voucher', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
gitTag: variables.gitTag,
|
||||
gitBuild: variables.gitBuild,
|
||||
user: user,
|
||||
userIcon: req.oidc ? crypto.createHash('sha256').update(user.email).digest('hex') : '',
|
||||
authDisabled: variables.authDisabled,
|
||||
info: req.flashMessage.type === 'info',
|
||||
info_text: req.flashMessage.message || '',
|
||||
error: req.flashMessage.type === 'error',
|
||||
error_text: req.flashMessage.message || '',
|
||||
kioskEnabled: variables.kioskEnabled,
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
notesConvert: notes,
|
||||
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
|
||||
printer_enabled: variables.printers !== '',
|
||||
voucher_types: types(variables.voucherTypes),
|
||||
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 true;
|
||||
}).filter((item) => {
|
||||
if(req.query.status === 'available') {
|
||||
return item.used === 0 && item.status !== 'EXPIRED';
|
||||
}
|
||||
|
||||
if(req.query.status === 'in-use') {
|
||||
return item.used > 0 && item.status !== 'EXPIRED';
|
||||
}
|
||||
|
||||
if(req.query.status === 'expired') {
|
||||
return item.status === 'EXPIRED';
|
||||
}
|
||||
|
||||
return true;
|
||||
}).filter((item) => {
|
||||
if(req.query.quota === 'multi-use') {
|
||||
return item.quota === 0;
|
||||
}
|
||||
|
||||
if(req.query.quota === 'single-use') {
|
||||
return item.quota !== 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).sort((a, b) => {
|
||||
if(req.query.sort === 'code') {
|
||||
if (a.code > b.code) return -1;
|
||||
if (a.code < b.code) return 1;
|
||||
}
|
||||
|
||||
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(req.query.sort === 'duration') {
|
||||
if (a.duration > b.duration) return -1;
|
||||
if (a.duration < b.duration) return 1;
|
||||
}
|
||||
|
||||
if(req.query.sort === 'status') {
|
||||
if (a.used > b.used) return -1;
|
||||
if (a.used < b.used) return 1;
|
||||
}
|
||||
}),
|
||||
updated: cache.updated,
|
||||
filters: {
|
||||
status: req.query.status,
|
||||
quota: req.query.quota
|
||||
},
|
||||
sort: req.query.sort
|
||||
});
|
||||
});
|
||||
app.get('/voucher/:id', [authorization.web], async (req, res) => {
|
||||
const voucher = cache.vouchers.find((e) => {
|
||||
return e._id === req.params.id;
|
||||
});
|
||||
const guests = cache.guests.filter((e) => {
|
||||
return e.voucher_id === req.params.id;
|
||||
});
|
||||
|
||||
if(voucher) {
|
||||
res.render('components/details', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
notesConvert: notes,
|
||||
voucher,
|
||||
guests,
|
||||
updated: cache.updated
|
||||
});
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
});
|
||||
app.get('/status', [authorization.web], async (req, res) => {
|
||||
const user = req.oidc ? await req.oidc.fetchUserInfo() : { email: 'admin' };
|
||||
|
||||
res.render('status', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
gitTag: variables.gitTag,
|
||||
gitBuild: variables.gitBuild,
|
||||
kioskEnabled: variables.kioskEnabled,
|
||||
user: user,
|
||||
userIcon: req.oidc ? crypto.createHash('sha256').update(user.email).digest('hex') : '',
|
||||
authDisabled: variables.authDisabled,
|
||||
status: status()
|
||||
});
|
||||
});
|
||||
app.get('/bulk/print', [authorization.web], async (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
res.render('components/bulk-print', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
timeConvert: time,
|
||||
bytesConvert: bytes,
|
||||
notesConvert: notes,
|
||||
languages,
|
||||
defaultLanguage: variables.translationDefault,
|
||||
printers: variables.printers.split(','),
|
||||
vouchers: cache.vouchers,
|
||||
updated: cache.updated
|
||||
});
|
||||
});
|
||||
app.post('/bulk/print', [authorization.web], async (req, res) => {
|
||||
if(variables.printers === '') {
|
||||
res.status(501).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!variables.printers.includes(req.body.printer)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!req.body.vouchers) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'No selected vouchers to print!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single checkboxes get send as string so conversion is needed
|
||||
if(typeof req.body.vouchers === 'string') {
|
||||
req.body.vouchers = [req.body.vouchers];
|
||||
}
|
||||
|
||||
const vouchers = req.body.vouchers.map((voucher) => {
|
||||
return cache.vouchers.find((e) => {
|
||||
return e._id === voucher;
|
||||
});
|
||||
});
|
||||
|
||||
if(!vouchers.includes(undefined)) {
|
||||
if(req.body.printer === 'pdf') {
|
||||
const buffers = await print.pdf(vouchers, req.body.language, true);
|
||||
const pdfData = Buffer.concat(buffers);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': Buffer.byteLength(pdfData),
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment;filename=bulk_vouchers_${new Date().getTime()}.pdf`
|
||||
}).end(pdfData);
|
||||
} else {
|
||||
let printSuccess = true;
|
||||
|
||||
for(let voucher = 0; voucher < vouchers.length; voucher++) {
|
||||
const printResult = await print.escpos(vouchers[voucher], req.body.language, req.body.printer).catch((e) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
});
|
||||
|
||||
if(!printResult) {
|
||||
printSuccess = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(printSuccess) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: `Vouchers send to printer!`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
}
|
||||
});
|
||||
app.get('/bulk/print', [authorization.web], bulk.print.get);
|
||||
app.post('/bulk/print', [authorization.web], bulk.print.post);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup API Routes
|
||||
*/
|
||||
if(variables.serviceApi) {
|
||||
app.get('/api', (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/types'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/languages'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/vouchers'
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
endpoint: '/api/voucher'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
app.get('/api/types', (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
types: types(variables.voucherTypes)
|
||||
}
|
||||
});
|
||||
});
|
||||
app.get('/api/languages', (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
languages: Object.keys(languages).map(language => {
|
||||
return {
|
||||
code: language,
|
||||
name: languages[language]
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
app.get('/api/vouchers', [authorization.api], async (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
vouchers: cache.vouchers.map((voucher) => {
|
||||
return {
|
||||
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
|
||||
};
|
||||
}),
|
||||
updated: cache.updated
|
||||
}
|
||||
});
|
||||
});
|
||||
app.post('/api/voucher', [authorization.api], async (req, res) => {
|
||||
// Verify valid body is sent
|
||||
if(!req.body || !req.body.type) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid Body!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
app.get('/api', api.api.get);
|
||||
app.get('/api/types', api.types.get);
|
||||
app.get('/api/languages', api.languages.get);
|
||||
|
||||
// Check if email body is set
|
||||
if(req.body.email) {
|
||||
// Check if email module is enabled
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(400).json({
|
||||
error: 'Email Not Configured!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if email body is correct
|
||||
if(!req.body.email.language || !req.body.email.address) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid Body!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if language is available
|
||||
if(!Object.keys(languages).includes(req.body.email.language)) {
|
||||
res.status(400).json({
|
||||
error: 'Unknown Language!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if type is implemented and valid
|
||||
const typeCheck = (variables.voucherTypes).split(';').includes(req.body.type);
|
||||
if(!typeCheck) {
|
||||
res.status(400).json({
|
||||
error: 'Unknown Type!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.body.type, true), 1, `||;;||api||;;||local||;;||`).catch((e) => {
|
||||
res.status(500).json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
// Update application cache
|
||||
await updateCache();
|
||||
|
||||
if(voucherCode) {
|
||||
// Locate voucher data within cache
|
||||
const voucherData = cache.vouchers.find(voucher => voucher.code === voucherCode.replaceAll('-', ''));
|
||||
if(!voucherData) {
|
||||
res.status(500).json({
|
||||
error: 'Invalid application cache!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we should send and email
|
||||
if(req.body.email) {
|
||||
// Send mail
|
||||
const emailResult = await mail.send(req.body.email.address, voucherData, req.body.email.language).catch((e) => {
|
||||
res.status(500).json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
// Verify is the email was sent successfully
|
||||
if(emailResult) {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: {
|
||||
id: voucherData._id,
|
||||
code: voucherCode
|
||||
},
|
||||
email: {
|
||||
status: 'SENT',
|
||||
address: req.body.email.address
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: {
|
||||
id: voucherData._id,
|
||||
code: voucherCode
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
app.get('/api/vouchers', [authorization.api], api.vouchers.get);
|
||||
app.post('/api/voucher', [authorization.api], api.voucher.post);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup default 404 message
|
||||
*/
|
||||
app.use((req, res) => {
|
||||
res.status(404);
|
||||
res.render('404', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||
});
|
||||
});
|
||||
app.use(error["404"]);
|
||||
|
||||
/**
|
||||
* Setup default 500 message
|
||||
*/
|
||||
app.use((err, req, res, next) => {
|
||||
log.error(err.stack);
|
||||
res.status(500);
|
||||
res.render('500', {
|
||||
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
|
||||
error: err.stack
|
||||
});
|
||||
});
|
||||
app.use(error["500"]);
|
||||
|
||||
/**
|
||||
* Disable powered by header for security reasons
|
||||
@@ -980,9 +209,19 @@ app.listen(3000, '0.0.0.0', async () => {
|
||||
log.info(`[App] Running on: 0.0.0.0:3000`);
|
||||
await updateCache();
|
||||
|
||||
// Run auto sync every 15 minutes
|
||||
// Run tasks every 15 minutes
|
||||
setInterval(async () => {
|
||||
log.info('[Auto Sync] Starting Sync...');
|
||||
log.debug('[Task][Sync] Starting...');
|
||||
await updateCache();
|
||||
|
||||
if(variables.taskCleanupExpired) {
|
||||
log.debug('[Task][Cleanup][Expired] Starting...');
|
||||
await cleanupExpired();
|
||||
}
|
||||
|
||||
if(variables.taskCleanupUnused) {
|
||||
log.debug('[Task][Cleanup][Unused] Starting...');
|
||||
await cleanupUnused();
|
||||
}
|
||||
}, 900000);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="<%= Object.keys(languages).length < 2 ? ' hidden' : '' %>">
|
||||
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
|
||||
<div class="mt-2">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
@@ -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">
|
||||
<% vouchers.forEach((voucher) => { %>
|
||||
<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">
|
||||
<label for="voucher-<%= voucher._id %>" class="voucher min-w-0 flex-auto">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
<% 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">
|
||||
Expired
|
||||
</div>
|
||||
<% } 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">
|
||||
In Use
|
||||
</div>
|
||||
@@ -64,37 +64,37 @@
|
||||
</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">
|
||||
<%= notesConvert(voucher.note).note %>
|
||||
<%= notesConvert(voucher.name).note %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
<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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</svg>
|
||||
<p class="whitespace-nowrap"><%= timeConvert(voucher.duration) %></p>
|
||||
<% if(voucher.qos_usage_quota) { %>
|
||||
<p class="whitespace-nowrap"><%= timeConvert(voucher.timeLimitMinutes) %></p>
|
||||
<% 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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</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>
|
||||
</label>
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
<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>
|
||||
<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">
|
||||
Expired
|
||||
</div>
|
||||
<% } 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">
|
||||
In Use
|
||||
</div>
|
||||
@@ -43,49 +43,53 @@
|
||||
<% } %>
|
||||
</dd>
|
||||
</div>
|
||||
<% if(notesConvert(voucher.note).note) { %>
|
||||
<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">Created</dt>
|
||||
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0 pointer-events-none no-underline text-inherit"><%= new Intl.DateTimeFormat('en-GB', {dateStyle: 'short', timeStyle: 'short', hour12: false}).format(new Date(voucher.createdAt)) %></dd>
|
||||
</div>
|
||||
<% if(voucher.name && notesConvert(voucher.name).note) { %>
|
||||
<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>
|
||||
<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>
|
||||
<% } %>
|
||||
<% if(notesConvert(voucher.note).source) { %>
|
||||
<% if(voucher.name && notesConvert(voucher.name).source) { %>
|
||||
<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>
|
||||
<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>
|
||||
<% } %>
|
||||
<% 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">
|
||||
<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>
|
||||
<% } %>
|
||||
<% 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">
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<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-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="bg-sky-700 px-4 py-6 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -24,7 +24,7 @@
|
||||
<input type="email" id="email" name="email" required class="mt-2 block w-full rounded-md border-0 py-1.5 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="<%= Object.keys(languages).length < 2 ? ' hidden' : '' %>">
|
||||
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
|
||||
<div class="mt-2">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<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-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="bg-sky-700 px-4 py-6 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -28,7 +28,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="<%= Object.keys(languages).length < 2 ? ' hidden' : '' %>">
|
||||
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
|
||||
<div class="mt-2">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
|
||||
@@ -140,16 +140,16 @@
|
||||
<% } %>
|
||||
<p style="font-family: sans-serif; font-size: 20px; font-weight: bold; margin: 0; margin-bottom: 15px;"><%= t('details') %></p>
|
||||
<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('duration') %>:</span> <%= timeConvert(voucher.duration, language) %></p>
|
||||
<% if(voucher.qos_usage_quota) { %>
|
||||
<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('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.timeLimitMinutes, language) %></p>
|
||||
<% 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.dataUsageLimitMBytes, 2) %></p>
|
||||
<% } %>
|
||||
<% if(voucher.qos_rate_max_down) { %>
|
||||
<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>
|
||||
<% 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.rxRateLimitKbps, 1, true) %></p>
|
||||
<% } %>
|
||||
<% if(voucher.qos_rate_max_up) { %>
|
||||
<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>
|
||||
<% 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.txRateLimitKbps, 1, true) %></p>
|
||||
<% } %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
<div id="timer-container" class="timer-progress bg-gray-200 dark:bg-gray-700">
|
||||
<div id="timer-bar" class="bg-sky-600 h-full"></div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<% if(kiosk_homepage) { %>
|
||||
<a href="<%= baseUrl %>/vouchers" class="p-2 fixed top-2 left-2 text-md bg-sky-700 text-white rounded-md flex items-center justify-center hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M3 4.25A2.25 2.25 0 0 1 5.25 2h5.5A2.25 2.25 0 0 1 13 4.25v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 0-.75-.75h-5.5a.75.75 0 0 0-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 0 0 .75-.75v-2a.75.75 0 0 1 1.5 0v2A2.25 2.25 0 0 1 10.75 18h-5.5A2.25 2.25 0 0 1 3 15.75V4.25Z" clip-rule="evenodd" />
|
||||
<path fill-rule="evenodd" d="M19 10a.75.75 0 0 0-.75-.75H8.704l1.048-.943a.75.75 0 1 0-1.004-1.114l-2.5 2.25a.75.75 0 0 0 0 1.114l2.5 2.25a.75.75 0 1 0 1.004-1.114l-1.048-.943h9.546A.75.75 0 0 0 19 10Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Admin UI
|
||||
</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<div class="fixed top-0 left-0 w-full h-full -z-20">
|
||||
@@ -63,7 +73,7 @@
|
||||
<div class="p-4">
|
||||
<% if(typeof voucherCode === 'undefined') { %>
|
||||
<div class="block">
|
||||
<form id="locale-form" class="mb-6" action="<%= baseUrl %>/kiosk" method="get">
|
||||
<form id="locale-form" class="mb-6<%= Object.keys(languages).length < 2 ? ' hidden' : '' %>" action="<%= baseUrl %>/kiosk" method="get">
|
||||
<label for="locale" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white"><%= t('language') %></label>
|
||||
<div class="mt-2">
|
||||
<select id="locale" name="locale" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
|
||||
@@ -146,17 +146,17 @@
|
||||
<ul role="list" class="divide-y divide-black/5 dark:divide-white/5">
|
||||
<% 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">
|
||||
<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">
|
||||
<h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
<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>
|
||||
<% 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">
|
||||
Expired
|
||||
</div>
|
||||
<% } 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">
|
||||
In Use
|
||||
</div>
|
||||
@@ -166,37 +166,37 @@
|
||||
</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">
|
||||
<%= notesConvert(voucher.note).note %>
|
||||
<%= notesConvert(voucher.name).note %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
<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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</svg>
|
||||
<p class="whitespace-nowrap"><%= timeConvert(voucher.duration) %></p>
|
||||
<% if(voucher.qos_usage_quota) { %>
|
||||
<p class="whitespace-nowrap"><%= timeConvert(voucher.timeLimitMinutes) %></p>
|
||||
<% 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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</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">
|
||||
<circle cx="1" cy="1" r="1" />
|
||||
</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>
|
||||
@@ -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" />
|
||||
</svg>
|
||||
</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="sr-only">Email Voucher Code</span>
|
||||
<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" />
|
||||
</svg>
|
||||
</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="sr-only">Print Voucher Code</span>
|
||||
<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" />
|
||||
</svg>
|
||||
</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="sr-only">Remove Voucher Code</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
|
||||
@@ -11,6 +11,8 @@ module.exports = {
|
||||
'UI_BACK_BUTTON',
|
||||
'PRINTER_TYPE',
|
||||
'PRINTER_IP',
|
||||
'KIOSK_VOUCHER_TYPE'
|
||||
'KIOSK_VOUCHER_TYPE',
|
||||
'UNIFI_USERNAME',
|
||||
'UNIFI_PASSWORD'
|
||||
]
|
||||
};
|
||||
|
||||
69
utils/cleanup.js
Normal file
69
utils/cleanup.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const cache = require('../modules/cache');
|
||||
const log = require('../modules/log');
|
||||
const unifi = require('../modules/unifi');
|
||||
|
||||
/**
|
||||
* Import own utils
|
||||
*/
|
||||
const {updateCache} = require('./cache');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Function to clean up expired vouchers
|
||||
*
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
cleanupExpired: () => {
|
||||
return new Promise(async (resolve) => {
|
||||
// Filter vouchers in cache
|
||||
const vouchers = cache.vouchers.filter((voucher) => {
|
||||
return voucher.expired;
|
||||
});
|
||||
|
||||
log.debug(`[Cleanup] Removing ${vouchers.length} voucher(s)...`);
|
||||
|
||||
// Remove vouchers
|
||||
for(let item = 0; item < vouchers.length; item++) {
|
||||
log.debug(`[Cleanup] Removing voucher: ${vouchers[item].id}`);
|
||||
await unifi.remove(vouchers[item].id);
|
||||
}
|
||||
|
||||
// Update cache
|
||||
await updateCache();
|
||||
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to clean up unused voucher that are still active after a day
|
||||
*
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
cleanupUnused: () => {
|
||||
return new Promise(async (resolve) => {
|
||||
const vouchers = cache.vouchers.filter((voucher) => {
|
||||
const today = new Date();
|
||||
const voucherDate = new Date(voucher.createdAt);
|
||||
voucherDate.setDate(voucherDate.getDate() + 1);
|
||||
|
||||
return voucherDate.getTime() < today.getTime();
|
||||
});
|
||||
|
||||
log.debug(`[Cleanup] Removing ${vouchers.length} voucher(s)...`);
|
||||
|
||||
for(let item = 0; item < vouchers.length; item++) {
|
||||
log.debug(`[Cleanup] Removing voucher: ${vouchers[item].id}...`);
|
||||
await unifi.remove(vouchers[item].id);
|
||||
}
|
||||
|
||||
// Update cache
|
||||
await updateCache();
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
146
utils/fetch.js
Normal file
146
utils/fetch.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Import vendor modules
|
||||
*/
|
||||
const querystring = require('node:querystring');
|
||||
const {Agent} = require('undici');
|
||||
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const cache = require('../modules/cache');
|
||||
const variables = require('../modules/variables');
|
||||
const log = require('../modules/log');
|
||||
|
||||
/**
|
||||
* Request a Controller Site UUID
|
||||
*
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
const getSiteUUID = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`https://${variables.unifiIp}:${variables.unifiPort}/proxy/network/integration/v1/sites?filter=internalReference.eq('${variables.unifiSiteId}')`, {
|
||||
headers: {
|
||||
'User-Agent': 'unifi-voucher-site',
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': variables.unifiToken
|
||||
},
|
||||
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: ${variables.unifiSiteId}.`);
|
||||
log.debug(response);
|
||||
reject(`Unknown site id: ${variables.unifiSiteId}`);
|
||||
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(cache.unifi.siteUUID === null) {
|
||||
log.debug('[UniFi] Requesting Site UUID...');
|
||||
|
||||
const siteUUID = await getSiteUUID().catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
if(siteUUID) {
|
||||
cache.unifi.siteUUID = siteUUID;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Define base request
|
||||
const request = {
|
||||
method,
|
||||
headers: {
|
||||
'User-Agent': 'unifi-voucher-site',
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': variables.unifiToken
|
||||
},
|
||||
dispatcher: new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// Add data to body when object is given
|
||||
if(data !== null) {
|
||||
request.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
fetch(`https://${variables.unifiIp}:${variables.unifiPort}/proxy/network/integration/v1/sites/${cache.unifi.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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,17 +1,38 @@
|
||||
/**
|
||||
* Import own modules
|
||||
*/
|
||||
const variables = require('../modules/variables');
|
||||
|
||||
/**
|
||||
* Filter hidden languages from the list
|
||||
*
|
||||
* @param languages
|
||||
* @returns {*}
|
||||
*/
|
||||
const filterLanguages = (languages) => {
|
||||
Object.keys(languages).forEach(language => {
|
||||
if(variables.translationHiddenLanguages.includes(language)) {
|
||||
delete languages[language];
|
||||
}
|
||||
});
|
||||
|
||||
return languages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Exports all languages
|
||||
*/
|
||||
module.exports = {
|
||||
en: 'English',
|
||||
br: 'Brazilian, Portuguese',
|
||||
cs: 'Czech',
|
||||
da: 'Danish',
|
||||
de: 'German',
|
||||
es: 'Spanish',
|
||||
fi: 'Finnish',
|
||||
fr: 'French',
|
||||
nl: 'Dutch',
|
||||
pl: 'Polish',
|
||||
pt: 'Portuguese',
|
||||
ru: 'Russian'
|
||||
};
|
||||
module.exports = filterLanguages({
|
||||
en: '🇺🇸 English',
|
||||
br: '🇧🇷 Brazilian, Portuguese',
|
||||
cs: '🇨🇿 Czech',
|
||||
da: '🇩🇰 Danish',
|
||||
de: '🇩🇪 German',
|
||||
es: '🇪🇸 Spanish',
|
||||
fi: '🇫🇮 Finnish',
|
||||
fr: '🇫🇷 French',
|
||||
nl: '🇳🇱 Dutch',
|
||||
pl: '🇵🇱 Polish',
|
||||
pt: '🇵🇹 Portuguese',
|
||||
ru: '🇷🇺 Russian'
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @returns {*}
|
||||
*/
|
||||
module.exports = (string) => {
|
||||
if(string === null) {
|
||||
if(string === null || typeof string === 'undefined') {
|
||||
return {
|
||||
note: null,
|
||||
source: null,
|
||||
|
||||
@@ -9,15 +9,15 @@ const variables = require('../modules/variables');
|
||||
module.exports = (voucher) => {
|
||||
let base = variables.unifiSsid !== '' ? variables.unifiSsidPassword !== '' ? 415 : 375 : 260;
|
||||
|
||||
if(voucher.qos_usage_quota) {
|
||||
if(voucher.dataUsageLimitMBytes) {
|
||||
base += 10;
|
||||
}
|
||||
|
||||
if(voucher.qos_rate_max_down) {
|
||||
if(voucher.rxRateLimitKbps) {
|
||||
base += 10;
|
||||
}
|
||||
|
||||
if(voucher.qos_rate_max_up) {
|
||||
if(voucher.txRateLimitKbps) {
|
||||
base += 10;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user