From f36fa1144a8f65f421cc887b4461da7d8f84d978 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Fri, 22 Aug 2025 18:27:14 +0200 Subject: [PATCH] Refactored server.js into separate controllers. Implemented `UNIFI_TOKEN` startup check within info.js. Implemented `KIOSK_HOMEPAGE` environment variable. Implemented new / redirect structure base on `KIOSK_HOMEPAGE` variable. Implemented Admin UI button within kiosk.ejs. Updated array.js with new deprecated variables. Updated docker-compose.yml. Updated README.md. --- .docs/images/integrations_example.png | Bin 0 -> 105097 bytes README.md | 21 +- controllers/api.js | 247 ++++++++ controllers/authentication.js | 85 +++ controllers/bulk.js | 111 ++++ controllers/error.js | 36 ++ controllers/kiosk.js | 168 +++++ controllers/status.js | 37 ++ controllers/voucher.js | 291 +++++++++ controllers/vouchers.js | 142 +++++ docker-compose.yml | 2 + modules/info.js | 8 + modules/variables.js | 1 + server.js | 858 ++------------------------ template/kiosk.ejs | 10 + utils/array.js | 4 +- 16 files changed, 1199 insertions(+), 822 deletions(-) create mode 100644 .docs/images/integrations_example.png create mode 100644 controllers/api.js create mode 100644 controllers/authentication.js create mode 100644 controllers/bulk.js create mode 100644 controllers/error.js create mode 100644 controllers/kiosk.js create mode 100644 controllers/status.js create mode 100644 controllers/voucher.js create mode 100644 controllers/vouchers.js diff --git a/.docs/images/integrations_example.png b/.docs/images/integrations_example.png new file mode 100644 index 0000000000000000000000000000000000000000..cc9ca2e2d75279ae0ddc29eb693128e6b6eaa4fe GIT binary patch literal 105097 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 (!RjkhcYNk!qgzPr52;J8cn-4 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 ZxJ3J?E~4EwR-4>3u(HOZ@`q1e{4Y#NZ8rb_ literal 0 HcmV?d00001 diff --git a/README.md b/README.md index f45d579..a7f4ca5 100644 --- a/README.md +++ b/README.md @@ -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 Key, Dream Machine, or UniFi OS software) - UniFi Access Point (AP) -- UniFi Local Account with 'Full Management' access +- UniFi Integrations API Key + +![UniFi Integrations API Key](.docs/images/integrations_example.png) [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 @@ -81,6 +80,8 @@ services: 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,6 +143,8 @@ 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 @@ -173,6 +176,7 @@ The structure of the file should use lowercase versions of the environment varia "unifi_port": 443, "unifi_username": "admin", "unifi_password": "password", + "unifi_token": "", "unifi_site_id": "default", "unifi_ssid": "", "unifi_ssid_password": "", @@ -201,6 +205,7 @@ 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 @@ -714,6 +719,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. diff --git a/controllers/api.js b/controllers/api.js new file mode 100644 index 0000000..aee3a79 --- /dev/null +++ b/controllers/api.js @@ -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 + } + } + }); + } + } + } + } +}; diff --git a/controllers/authentication.js b/controllers/authentication.js new file mode 100644 index 0000000..6553068 --- /dev/null +++ b/controllers/authentication.js @@ -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'] : ''}/`); + } + } + } +}; diff --git a/controllers/bulk.js b/controllers/bulk.js new file mode 100644 index 0000000..608c8e8 --- /dev/null +++ b/controllers/bulk.js @@ -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'] : '' + }); + } + } + } +}; diff --git a/controllers/error.js b/controllers/error.js new file mode 100644 index 0000000..790e804 --- /dev/null +++ b/controllers/error.js @@ -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 + }); + } +}; diff --git a/controllers/kiosk.js b/controllers/kiosk.js new file mode 100644 index 0000000..f1884d1 --- /dev/null +++ b/controllers/kiosk.js @@ -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 + }); + } + } + } + } +}; diff --git a/controllers/status.js b/controllers/status.js new file mode 100644 index 0000000..9e925c4 --- /dev/null +++ b/controllers/status.js @@ -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() + }); + } +}; diff --git a/controllers/voucher.js b/controllers/voucher.js new file mode 100644 index 0000000..b70042b --- /dev/null +++ b/controllers/voucher.js @@ -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'] : '' + }); + } + } + } +}; diff --git a/controllers/vouchers.js b/controllers/vouchers.js new file mode 100644 index 0000000..1304f01 --- /dev/null +++ b/controllers/vouchers.js @@ -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 + }); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index c6d9733..45a8bdc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: UNIFI_PORT: 443 UNIFI_USERNAME: 'admin' UNIFI_PASSWORD: 'password' + UNIFI_TOKEN: '' UNIFI_SITE_ID: 'default' UNIFI_SSID: '' UNIFI_SSID_PASSWORD: '' @@ -36,5 +37,6 @@ services: KIOSK_VOUCHER_TYPES: '480,1,,,;' KIOSK_NAME_REQUIRED: 'false' KIOSK_PRINTER: '' + KIOSK_HOMEPAGE: 'false' LOG_LEVEL: 'info' TRANSLATION_DEBUG: 'false' diff --git a/modules/info.js b/modules/info.js index 7c317d6..ad9045a 100644 --- a/modules/info.js +++ b/modules/info.js @@ -130,4 +130,12 @@ module.exports = () => { if(variables.unifiUsername.includes('@')) { log.error('[UniFi] Incorrect username detected! UniFi Cloud credentials are not supported!'); } + + /** + * Check if UniFi Token is set + */ + if(variables.unifiToken === '') { + log.error('[UniFi] Integration API Key is not set within UNIFI_TOKEN environment variable!'); + process.exit(1); + } }; diff --git a/modules/variables.js b/modules/variables.js index dbcec21..f245935 100644 --- a/modules/variables.js +++ b/modules/variables.js @@ -45,6 +45,7 @@ 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', translationDebug: config('translation_debug') || (process.env.TRANSLATION_DEBUG === 'true') || false, diff --git a/server.js b/server.js index 5d9297d..b2f642a 100644 --- a/server.js +++ b/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,22 @@ 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'); /** * Setup Express app @@ -138,835 +137,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 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 - }); - }); - 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_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'] : '' - }); - } - }); - 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.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 - } - }); - }); - 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 diff --git a/template/kiosk.ejs b/template/kiosk.ejs index 8b3a606..22e18a8 100644 --- a/template/kiosk.ejs +++ b/template/kiosk.ejs @@ -32,6 +32,16 @@
+ <% } else { %> + <% if(kiosk_homepage) { %> + + + + + + Admin UI + + <% } %> <% } %>
diff --git a/utils/array.js b/utils/array.js index 70e466b..7106e02 100644 --- a/utils/array.js +++ b/utils/array.js @@ -11,6 +11,8 @@ module.exports = { 'UI_BACK_BUTTON', 'PRINTER_TYPE', 'PRINTER_IP', - 'KIOSK_VOUCHER_TYPE' + 'KIOSK_VOUCHER_TYPE', + 'UNIFI_USERNAME', + 'UNIFI_PASSWORD' ] };