diff --git a/.docs/images/integrations_example.png b/.docs/images/integrations_example.png new file mode 100644 index 0000000..cc9ca2e Binary files /dev/null and b/.docs/images/integrations_example.png differ 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 + + [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 @@