mirror of
https://github.com/glenndehaan/unifi-voucher-site.git
synced 2026-04-05 08:54:17 -04:00
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:
BIN
.docs/images/kiosk_example.png
Normal file
BIN
.docs/images/kiosk_example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 791 KiB |
51
README.md
51
README.md
@@ -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
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
BIN
public/images/kiosk_bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 259 KiB |
102
server.js
102
server.js
@@ -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
217
template/kiosk.ejs
Normal 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>
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -172,6 +172,20 @@
|
|||||||
<%= status.email.details %> <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 %> <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 %> <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">
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user