Added the kiosk_example.png. Updated the README.md. Implemented timer styles within style.css. Added kiosk output within info.js. Set default locale to 'en' for mail.js. Implemented KIOSK_ENABLED and KIOSK_VOUCHER_TYPE environment variables within variables.js. Added kiosk_bg.jpg. Updated navigation.ejs with kiosk link. Added kiosk.ejs template. Updated status.ejs and status.js with kiosk module status. Updated docker-compose.yml. Added /kiosk routes to server.js. Added kioskEnabled states to required routes in server.js

This commit is contained in:
Glenn de Haan
2025-03-14 19:18:07 +01:00
parent bd047558e5
commit 30ef63f277
13 changed files with 448 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

View File

@@ -124,6 +124,11 @@ services:
SMTP_USERNAME: '' SMTP_USERNAME: ''
# SMTP Mail password (optional) # SMTP Mail password (optional)
SMTP_PASSWORD: '' SMTP_PASSWORD: ''
# Enable/disable the kiosk page on /kiosk
KIOSK_ENABLED: 'false'
# Kiosk Voucher Type, format: expiration in minutes (required),single-use or multi-use vouchers value - '0' is for multi-use (unlimited) - '1' is for single-use - 'N' is for multi-use (Nx) (optional),upload speed limit in kbps (optional),download speed limit in kbps (optional),data transfer limit in MB (optional)
# To skip a parameter just but nothing in between the comma's
KIOSK_VOUCHER_TYPE: '480,1,,,'
# Sets the application Log Level (Valid Options: error|warn|info|debug|trace) # Sets the application Log Level (Valid Options: error|warn|info|debug|trace)
LOG_LEVEL: 'info' LOG_LEVEL: 'info'
# Sets the default translation for dropdowns # Sets the default translation for dropdowns
@@ -541,6 +546,52 @@ Once the SMTP environment variables are configured, the email feature will be av
![Example Email](.docs/images/email_example.png) ![Example Email](.docs/images/email_example.png)
## Kiosk Functionality
The UniFi Voucher Site includes a **Kiosk Mode**, allowing users to generate their own vouchers via a self-service interface. This is ideal for public areas, such as cafés, hotels, and co-working spaces, where users can obtain internet access without staff intervention.
To enable the kiosk functionality, set the following environment variables:
```env
KIOSK_ENABLED: 'true'
KIOSK_VOUCHER_TYPE: '480,1,,,'
```
### Configuration
- **`KIOSK_ENABLED`**:
- Set to `'true'` to enable the kiosk page, making it accessible at `/kiosk`.
- Set to `'false'` to disable the kiosk functionality.
- **`KIOSK_VOUCHER_TYPE`**: Defines the voucher properties for kiosk-generated vouchers. The format consists of the following parameters:
```text
expiration_in_minutes,single_use_or_multi_use,upload_speed_limit_kbps,download_speed_limit_kbps,data_transfer_limit_MB
```
- **Expiration (required)**: The validity period of the voucher in minutes. Example: `480` (8 hours).
- **Voucher Type (required)**: Defines if the voucher is single-use or multi-use:
- `'0'` → Multi-use (unlimited)
- `'1'` → Single-use
- `'N'` → Multi-use (N times) (e.g., `'3'` allows 3 uses)
- **Upload Speed Limit (optional)**: Maximum upload speed in Kbps. Leave empty to disable.
- **Download Speed Limit (optional)**: Maximum download speed in Kbps. Leave empty to disable.
- **Data Transfer Limit (optional)**: Total data limit in MB. Leave empty to disable.
### Usage
Once enabled, the kiosk page is available at:
```
http://localhost:3000/kiosk
```
Users can visit this URL and generate a voucher without administrative intervention.
### Example Kiosk
![Example Kiosk](.docs/images/kiosk_example.png)
## Translations ## Translations
The UniFi Voucher Site supports multiple languages, and we're actively working to expand the list of available translations. To facilitate this, we use **Crowdin**, a platform that allows people from around the world to help translate and improve the localization of the project. The UniFi Voucher Site supports multiple languages, and we're actively working to expand the list of available translations. To facilitate this, we use **Crowdin**, a platform that allows people from around the world to help translate and improve the localization of the project.

View File

@@ -31,3 +31,36 @@
cursor: pointer; cursor: pointer;
} }
} }
.timer-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
overflow: hidden;
height: 6px;
z-index: 100;
}
.timer-bar {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 100%;
transform-origin: right;
}
#timer-bar {
transform: translateX(-100%);
}
@keyframes countdown {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
.animate-countdown {
animation: countdown 60s linear forwards;
will-change: transform;
}

View File

@@ -32,5 +32,7 @@ services:
SMTP_SECURE: '' SMTP_SECURE: ''
SMTP_USERNAME: '' SMTP_USERNAME: ''
SMTP_PASSWORD: '' SMTP_PASSWORD: ''
KIOSK_ENABLED: 'false'
KIOSK_VOUCHER_TYPE: '480,1,,,'
LOG_LEVEL: 'info' LOG_LEVEL: 'info'
TRANSLATION_DEBUG: 'false' TRANSLATION_DEBUG: 'false'

View File

@@ -106,6 +106,15 @@ module.exports = () => {
log.info(`[Email] Disabled!`); log.info(`[Email] Disabled!`);
} }
/**
* Log kiosk status
*/
if(variables.kioskEnabled) {
const kioskType = types(variables.kioskVoucherType, true);
log.info('[Kiosk] Enabled!');
log.info(`[Kiosk][Type] ${time(kioskType.expiration)}, ${kioskType.usage === '1' ? 'single-use' : kioskType.usage === '0' ? 'multi-use (unlimited)' : `multi-use (${kioskType.usage}x)`}${typeof kioskType.upload === "undefined" && typeof kioskType.download === "undefined" && typeof kioskType.megabytes === "undefined" ? ', no limits' : `${typeof kioskType.upload !== "undefined" ? `, upload bandwidth limit: ${kioskType.upload} kb/s` : ''}${typeof kioskType.download !== "undefined" ? `, download bandwidth limit: ${kioskType.download} kb/s` : ''}${typeof kioskType.megabytes !== "undefined" ? `, quota limit: ${kioskType.megabytes} mb` : ''}`}`);
}
/** /**
* Log controller * Log controller
*/ */

View File

@@ -47,7 +47,7 @@ module.exports = {
* @param language * @param language
* @return {Promise<unknown>} * @return {Promise<unknown>}
*/ */
send: (to, voucher, language) => { send: (to, voucher, language = 'en') => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// Create new translator // Create new translator
const t = translation('email', language); const t = translation('email', language);

View File

@@ -40,6 +40,8 @@ module.exports = {
smtpSecure: config('smtp_secure') || (process.env.SMTP_SECURE === 'true') || false, smtpSecure: config('smtp_secure') || (process.env.SMTP_SECURE === 'true') || false,
smtpUsername: config('smtp_username') || process.env.SMTP_USERNAME || '', smtpUsername: config('smtp_username') || process.env.SMTP_USERNAME || '',
smtpPassword: config('smtp_password') || process.env.SMTP_PASSWORD || '', smtpPassword: config('smtp_password') || process.env.SMTP_PASSWORD || '',
kioskEnabled: config('kiosk_enabled') || (process.env.KIOSK_ENABLED === 'true') || false,
kioskVoucherType: config('kiosk_voucher_type') || process.env.KIOSK_VOUCHER_TYPE || '480,1,,,',
logLevel: config('log_level') || process.env.LOG_LEVEL || 'info', logLevel: config('log_level') || process.env.LOG_LEVEL || 'info',
translationDefault: config('translation_default') || process.env.TRANSLATION_DEFAULT || 'en', translationDefault: config('translation_default') || process.env.TRANSLATION_DEFAULT || 'en',
translationDebug: config('translation_debug') || (process.env.TRANSLATION_DEBUG === 'true') || false, translationDebug: config('translation_debug') || (process.env.TRANSLATION_DEBUG === 'true') || false,

BIN
public/images/kiosk_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

102
server.js
View File

@@ -20,6 +20,7 @@ const unifi = require('./modules/unifi');
const print = require('./modules/print'); const print = require('./modules/print');
const mail = require('./modules/mail'); const mail = require('./modules/mail');
const oidc = require('./modules/oidc'); const oidc = require('./modules/oidc');
const qr = require('./modules/qr');
/** /**
* Import own middlewares * Import own middlewares
@@ -139,6 +140,105 @@ app.get('/', (req, res) => {
// Check if web service is enabled // Check if web service is enabled
if(variables.serviceWeb) { if(variables.serviceWeb) {
app.get('/kiosk', (req, res) => {
// Check if kiosk is disabled
if(!variables.kioskEnabled) {
res.status(501).send();
return;
}
res.render('kiosk', {
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
error: req.flashMessage.type === 'error',
error_text: req.flashMessage.message || ''
});
});
app.post('/kiosk', 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.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).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', {
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 {
// Create voucher code
const voucherCode = await unifi.create(types(variables.kioskVoucherType, true)).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;
}
res.render('kiosk', {
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) => { app.get('/login', (req, res) => {
// Check if authentication is disabled // Check if authentication is disabled
if (variables.authDisabled) { if (variables.authDisabled) {
@@ -416,6 +516,7 @@ if(variables.serviceWeb) {
error: req.flashMessage.type === 'error', error: req.flashMessage.type === 'error',
error_text: req.flashMessage.message || '', error_text: req.flashMessage.message || '',
uiBackButton: variables.uiBackButton, uiBackButton: variables.uiBackButton,
kioskEnabled: variables.kioskEnabled,
timeConvert: time, timeConvert: time,
bytesConvert: bytes, bytesConvert: bytes,
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '', email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
@@ -507,6 +608,7 @@ if(variables.serviceWeb) {
gitTag: variables.gitTag, gitTag: variables.gitTag,
gitBuild: variables.gitBuild, gitBuild: variables.gitBuild,
uiBackButton: variables.uiBackButton, uiBackButton: variables.uiBackButton,
kioskEnabled: variables.kioskEnabled,
user: user, user: user,
userIcon: req.oidc ? crypto.createHash('sha256').update(user.email).digest('hex') : '', userIcon: req.oidc ? crypto.createHash('sha256').update(user.email).digest('hex') : '',
authDisabled: variables.authDisabled, authDisabled: variables.authDisabled,

217
template/kiosk.ejs Normal file
View File

@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-gray-100 dark:bg-gray-900">
<head>
<meta name="format-detection" content="telephone=no">
<title>Kiosk | UniFi Voucher</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="description" content="UniFi Voucher Site is a web-based platform for generating and managing UniFi network guest vouchers">
<meta name="author" content="Glenn de Haan">
<meta property="og:title" content="Kiosk | UniFi Voucher"/>
<meta property="og:type" content="website"/>
<meta property="og:description" content="UniFi Voucher Site is a web-based platform for generating and managing UniFi network guest vouchers"/>
<link rel="manifest" href="<%= baseUrl %>/manifest.json">
<link rel="shortcut icon" href="<%= baseUrl %>/images/favicon.ico">
<link rel="apple-touch-icon" href="<%= baseUrl %>/images/icon/logo_256x256.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#139CDA">
<link rel="preload" href="<%= baseUrl %>/images/logo.png" as="image">
<link rel="preload" href="<%= baseUrl %>/dist/style.css" as="style">
<link href="<%= baseUrl %>/dist/style.css" rel="stylesheet">
</head>
<body class="min-h-screen flex flex-col items-center justify-center p-4">
<% if(typeof voucherCode !== 'undefined') { %>
<div id="timer-container" class="timer-progress bg-gray-200 dark:bg-gray-700">
<div id="timer-bar" class="bg-sky-600 h-full"></div>
</div>
<% } %>
<div class="fixed top-0 left-0 w-full h-full -z-20">
<img src="<%= baseUrl %>/images/kiosk_bg.jpg" alt="Kiosk Background" class="w-full h-full object-cover"/>
</div>
<div class="fixed top-0 left-0 w-full h-full -z-10 bg-white/70 dark:bg-black/70"></div>
<div class="w-full max-w-md bg-white dark:bg-gray-800 rounded-lg border border-black/5 dark:border-white/5 shadow-sm z-10 relative">
<div class="p-4 border-b border-black/5 dark:border-white/5">
<img class="mx-auto h-24 w-auto" width="48" height="48" alt="UniFi Voucher Site Logo" src="<%= baseUrl %>/images/logo.png">
<h1 class="mt-4 text-2xl font-semibold text-center text-gray-900 dark:text-white">WiFi Voucher</h1>
<% if(error) { %>
<div class="mt-5 rounded-md bg-red-700 p-4">
<div class="flex">
<div class="shrink-0">
<svg class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-white"><%= error_text %></h3>
</div>
</div>
</div>
<% } %>
</div>
<div class="p-4">
<% if(typeof voucherCode === 'undefined') { %>
<div class="block">
<form id="voucher-form" action="<%= baseUrl %>/kiosk" method="post">
<button id="generate-button" class="w-full h-16 text-lg bg-sky-700 text-white rounded-md flex items-center justify-center font-medium hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
<line x1="12" y1="20" x2="12" y2="20"></line>
</svg>
Generate WiFi Voucher
</button>
</form>
<div id="generating-button" class="hidden w-full h-16 text-lg bg-sky-600 text-white rounded-md flex items-center justify-center font-medium">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="2" x2="12" y2="6"></line>
<line x1="12" y1="18" x2="12" y2="22"></line>
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
<line x1="2" y1="12" x2="6" y2="12"></line>
<line x1="18" y1="12" x2="22" y2="12"></line>
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
</svg>
Generating Voucher ...
</div>
</div>
<% } else { %>
<div class="space-y-4">
<div class="rounded-lg border-0 ring-1 ring-inset ring-gray-300 dark:ring-white/10 p-4 bg-white dark:bg-gray-900">
<h2 class=" text-center text-sm mb-2 text-gray-900 dark:text-white">
Use this code when connecting:
</h2>
<div class="text-center text-2xl font-mono tracking-wider mb-2 text-gray-900 dark:text-white">
<%= voucherCode %>
</div>
<div class="text-center text-sm text-gray-900 dark:text-white mb-2">
<% if(unifiSsidPassword !== '') { %>
Connect to: <strong><%= unifiSsid %></strong>,<br/>
Password: <strong><%= unifiSsidPassword %></strong> or,<br/>
<% } else { %>
Connect to: <strong><%= unifiSsid %></strong> or,<br/>
<% } %>
Scan to connect:
</div>
<div class="flex justify-center">
<img src="<%= qr %>" alt="Scan to Connect QR Code"/>
</div>
</div>
<% if(email_enabled) { %>
<form class="grid gap-4" id="email-form" action="<%= baseUrl %>/kiosk" method="post" enctype="multipart/form-data">
<% if(typeof email === 'undefined') { %>
<div class="space-y-2">
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Email Voucher (optional)
</label>
<input id="email" type="email" name="email" placeholder="Enter your email address" class="block w-full rounded-md border-0 py-1.5 text-gray-900 dark:text-white bg-white dark:bg-gray-900 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6" required/>
</div>
<input id="voucher" type="hidden" name="id" value="<%= voucherId %>"/>
<input id="voucher" type="hidden" name="code" value="<%= voucherCode %>"/>
<button id="email-button" type="submit" class="w-full bg-sky-700 text-white py-2 rounded-md flex items-center justify-center hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2"></rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path>
</svg>
Send Voucher
</button>
<div id="sending-button" class="hidden w-full bg-sky-700/80 text-white py-2 rounded-md flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="2" x2="12" y2="6"></line>
<line x1="12" y1="18" x2="12" y2="22"></line>
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
<line x1="2" y1="12" x2="6" y2="12"></line>
<line x1="18" y1="12" x2="22" y2="12"></line>
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
</svg>
Sending Email ...
</div>
<% } else { %>
<div class="w-full bg-green-600 text-white py-2 rounded-md flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Email Sent
</div>
<% } %>
</form>
<% } %>
</div>
<% } %>
</div>
<% if(typeof voucherCode !== 'undefined') { %>
<div class="p-4 border-t border-black/5 dark:border-white/5 flex justify-center">
<a href="<%= baseUrl %>/kiosk" class="px-4 py-2 border dark:border-gray-700 rounded-md text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back
</a>
</div>
<% } %>
</div>
<script type="application/javascript">
const voucherForm = document.querySelector('#voucher-form');
const emailForm = document.querySelector('#email-form');
const generateButton = document.querySelector('#generate-button');
const generatingButton = document.querySelector('#generating-button');
const emailButton = document.querySelector('#email-button');
const sendingButton = document.querySelector('#sending-button');
if(voucherForm) {
voucherForm.addEventListener('submit', () => {
generateButton.classList.add('hidden');
generatingButton.classList.remove('hidden');
});
}
if(emailForm) {
emailForm.addEventListener('submit', () => {
emailButton.classList.add('hidden');
sendingButton.classList.remove('hidden');
});
}
</script>
<% if(typeof voucherCode !== 'undefined') { %>
<script type="application/javascript">
const timerContainer = document.querySelector('#timer-container');
const timerBar = document.querySelector('#timer-bar');
timerBar.classList.remove('animate-countdown');
void timerBar.offsetWidth;
timerBar.classList.add('animate-countdown');
let timeLeft = 60;
setInterval(() => {
timeLeft--;
if (timeLeft <= 0) {
window.location.href = '<%= baseUrl %>/kiosk';
}
}, 1000);
</script>
<% } %>
</body>
</html>

View File

@@ -45,6 +45,14 @@
Logged in as:<br> Logged in as:<br>
<span class="font-medium"><%= user.email %></span> <span class="font-medium"><%= user.email %></span>
</div> </div>
<% if(kioskEnabled) { %>
<a href="<%= baseUrl %>/kiosk" aria-label="UniFi Voucher Status" type="button" class="flex px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 items-center">
<svg class="mr-3 h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M2.25 5.25a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3V15a3 3 0 0 1-3 3h-3v.257c0 .597.237 1.17.659 1.591l.621.622a.75.75 0 0 1-.53 1.28h-9a.75.75 0 0 1-.53-1.28l.621-.622a2.25 2.25 0 0 0 .659-1.59V18h-3a3 3 0 0 1-3-3V5.25Zm1.5 0v7.5a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5v-7.5a1.5 1.5 0 0 0-1.5-1.5H5.25a1.5 1.5 0 0 0-1.5 1.5Z" clip-rule="evenodd" />
</svg>
Kiosk
</a>
<% } %>
<a href="<%= baseUrl %>/status" aria-label="UniFi Voucher Status" type="button" class="flex px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 items-center"> <a href="<%= baseUrl %>/status" aria-label="UniFi Voucher Status" type="button" class="flex px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 items-center">
<svg class="mr-3 h-5 w-5" viewBox="0 0 24 24" fill="currentColor"> <svg class="mr-3 h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M10.5 3.798v5.02a3 3 0 0 1-.879 2.121l-2.377 2.377a9.845 9.845 0 0 1 5.091 1.013 8.315 8.315 0 0 0 5.713.636l.285-.071-3.954-3.955a3 3 0 0 1-.879-2.121v-5.02a23.614 23.614 0 0 0-3 0Zm4.5.138a.75.75 0 0 0 .093-1.495A24.837 24.837 0 0 0 12 2.25a25.048 25.048 0 0 0-3.093.191A.75.75 0 0 0 9 3.936v4.882a1.5 1.5 0 0 1-.44 1.06l-6.293 6.294c-1.62 1.621-.903 4.475 1.471 4.88 2.686.46 5.447.698 8.262.698 2.816 0 5.576-.239 8.262-.697 2.373-.406 3.092-3.26 1.47-4.881L15.44 9.879A1.5 1.5 0 0 1 15 8.818V3.936Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10.5 3.798v5.02a3 3 0 0 1-.879 2.121l-2.377 2.377a9.845 9.845 0 0 1 5.091 1.013 8.315 8.315 0 0 0 5.713.636l.285-.071-3.954-3.955a3 3 0 0 1-.879-2.121v-5.02a23.614 23.614 0 0 0-3 0Zm4.5.138a.75.75 0 0 0 .093-1.495A24.837 24.837 0 0 0 12 2.25a25.048 25.048 0 0 0-3.093.191A.75.75 0 0 0 9 3.936v4.882a1.5 1.5 0 0 1-.44 1.06l-6.293 6.294c-1.62 1.621-.903 4.475 1.471 4.88 2.686.46 5.447.698 8.262.698 2.816 0 5.576-.239 8.262-.697 2.373-.406 3.092-3.26 1.47-4.881L15.44 9.879A1.5 1.5 0 0 1 15 8.818V3.936Z" clip-rule="evenodd" />

View File

@@ -172,6 +172,20 @@
<%= status.email.details %>&nbsp;<a href="<%= status.email.info %>" class="italic text-xs underline" aria-label="More Info Link" target="_blank" rel="noreferrer noopener">More Info</a> <%= status.email.details %>&nbsp;<a href="<%= status.email.info %>" class="italic text-xs underline" aria-label="More Info Link" target="_blank" rel="noreferrer noopener">More Info</a>
</td> </td>
</tr> </tr>
<tr class="border-b border-gray-300 dark:border-white/10 bg-white dark:bg-white/5">
<td class="p-4 align-middle font-medium flex items-center gap-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M2.25 5.25a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3V15a3 3 0 0 1-3 3h-3v.257c0 .597.237 1.17.659 1.591l.621.622a.75.75 0 0 1-.53 1.28h-9a.75.75 0 0 1-.53-1.28l.621-.622a2.25 2.25 0 0 0 .659-1.59V18h-3a3 3 0 0 1-3-3V5.25Zm1.5 0v7.5a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5v-7.5a1.5 1.5 0 0 0-1.5-1.5H5.25a1.5 1.5 0 0 0-1.5 1.5Z" clip-rule="evenodd" />
</svg>
Kiosk
</td>
<td class="p-4 align-middle">
<%- include('partials/tag', {status: status.kiosk.status}) %>
</td>
<td class="p-4 align-middle">
<%= status.kiosk.details %>&nbsp;<a href="<%= status.kiosk.info %>" class="italic text-xs underline" aria-label="More Info Link" target="_blank" rel="noreferrer noopener">More Info</a>
</td>
</tr>
<tr class="border-b border-gray-300 dark:border-white/10 bg-white dark:bg-white/5"> <tr class="border-b border-gray-300 dark:border-white/10 bg-white dark:bg-white/5">
<td class="p-4 align-middle font-medium flex items-center gap-2"> <td class="p-4 align-middle font-medium flex items-center gap-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"> <svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">

View File

@@ -80,6 +80,15 @@ module.exports = () => {
info: 'https://github.com/glenndehaan/unifi-voucher-site#email-functionality', info: 'https://github.com/glenndehaan/unifi-voucher-site#email-functionality',
modules: {} modules: {}
}, },
kiosk: {
status: {
text: variables.kioskEnabled ? 'Enabled' : 'Disabled',
state: variables.kioskEnabled ? 'green' : 'red'
},
details: variables.kioskEnabled ? `Kiosk service enabled on http://0.0.0.0:3000/kiosk.` : 'Kiosk service not enabled.',
info: 'https://github.com/glenndehaan/unifi-voucher-site#kiosk-functionality',
modules: {}
},
authentication: { authentication: {
status: { status: {
text: !variables.authDisabled ? 'Enabled' : 'Disabled', text: !variables.authDisabled ? 'Enabled' : 'Disabled',