Implemented voucher types. Updated the README.md

This commit is contained in:
Glenn de Haan
2022-11-14 16:36:29 +01:00
parent 006343a248
commit 65c29e2e83
8 changed files with 120 additions and 15 deletions

View File

@@ -6,14 +6,14 @@ A small UniFi Voucher Site for simple voucher creation
## Structure
- ES6 Javascript
- SCSS
- ExpressJS
- Node UniFi
- Webpack
- Tailwind
## Development Usage
- Install NodeJS 16.0 or higher.
- Run `npm ci` in the root folder
- Run `npm start` in the root folder
- Run `npm start` & `npm run tailwind` in the root folder
Then open up your favorite browser and go to http://localhost:3000/
@@ -25,6 +25,32 @@ Then open up your favorite browser and go to http://localhost:3000/
## Docker
- Code from master is build by Docker Hub
- Builds can be pulled by using this command: `docker pull glenndehaan/unifi-voucher-site`
- An example docker compose file can be found below:
```yaml
version: '3'
services:
app:
image: glenndehaan/unifi-voucher-site
ports:
- "8081:3000"
environment:
# The IP address to your UniFi OS Console
UNIFI_IP: '192.168.1.1'
# The port of your UniFi OS Console, this could be 443 or 8443
UNIFI_PORT: 443
# The username of a local UniFi OS account
UNIFI_USERNAME: 'admin'
# The password of a local UniFi OS account
UNIFI_PASSWORD: 'password'
# The UniFi Site ID
UNIFI_SITE_ID: 'default'
# The 'password' used to log in to this voucher portal
SECURITY_CODE: '0000'
# Voucher Types, format: expiration in minutes (required),single-use or multi-use vouchers value - '0' is for multi-use - '1' is for single-use (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
# After a voucher type add a semicolon, after the semicolon you can start a new voucher type
VOUCHER_TYPES: '480,0,,,;'
```
## License

View File

@@ -11,3 +11,4 @@ services:
UNIFI_PASSWORD: 'password'
UNIFI_SITE_ID: 'default'
SECURITY_CODE: '0000'
VOUCHER_TYPES: '480,0,,,;'

15
modules/logo.js Normal file
View File

@@ -0,0 +1,15 @@
/**
* Output the ascii logo
*/
module.exports = () => {
console.log(` __ __ _ _______ _ __ __ `);
console.log(` / / / /___ (_) ____(_) | | / /___ __ _______/ /_ ___ _____`);
console.log(` / / / / __ \\/ / /_ / / | | / / __ \\/ / / / ___/ __ \\/ _ \\/ ___/`);
console.log(`/ /_/ / / / / / __/ / / | |/ / /_/ / /_/ / /__/ / / / __/ / `);
console.log(`\\____/_/ /_/_/_/ /_/ |___/\\____/\\__,_/\\___/_/ /_/\\___/_/ `);
console.log('');
console.log(' UniFi Voucher ');
console.log(' By: Glenn de Haan ');
console.log(' https://github.com/glenndehaan/unifi-voucher-site ');
console.log('');
}

20
modules/types.js Normal file
View File

@@ -0,0 +1,20 @@
/**
* Returns an array or object of voucher type(s)
*
* @param string
* @param single
* @returns {*}
*/
module.exports = (string, single = false) => {
if(single) {
const match = string.match(/^(?<expiration>\d+)?,(?<usage>\d+)?,(?<upload>\d+)?,(?<download>\d+)?,(?<megabytes>\d+)?/);
return match.groups.expiration ? {...match.groups, raw: string} : undefined;
}
const types = string.split(';');
return types.filter(n => n).map((type) => {
const match = type.match(/^(?<expiration>\d+)?,(?<usage>\d+)?,(?<upload>\d+)?,(?<download>\d+)?,(?<megabytes>\d+)?/);
return match.groups.expiration ? {...match.groups, raw: type} : undefined;
}).filter(n => n);
}

View File

@@ -26,13 +26,14 @@ const controller = new unifi.Controller({host: config.unifi.ip, port: config.uni
/**
* Exports the UniFi voucher function
*
* @param type
* @returns {Promise<unknown>}
*/
module.exports = () => {
module.exports = (type) => {
return new Promise((resolve) => {
controller.login(config.unifi.username, config.unifi.password).then(() => {
controller.getSitesStats().then(() => {
controller.createVouchers(480).then((voucher_data) => {
controller.createVouchers(type.expiration, 1, type.usage === 1 ? 1 : 0, null, typeof type.upload !== "undefined" ? type.upload : null, typeof type.download !== "undefined" ? type.download : null, typeof type.megabytes !== "undefined" ? type.megabytes : null).then((voucher_data) => {
controller.getVouchers(voucher_data[0].create_time).then((voucher_data_complete) => {
const voucher = `${[voucher_data_complete[0].code.slice(0, 5), '-', voucher_data_complete[0].code.slice(5)].join('')}`;
resolve(voucher);

View File

@@ -9,12 +9,33 @@ const app = express();
/**
* Import own modules
*/
const logo = require('./modules/logo');
const types = require('./modules/types');
const unifi = require('./modules/unifi');
/**
* Define global functions
* Define global functions and variables
*/
const random = (min, max) => Math.floor(Math.random() * (max - min)) + min;
const voucherTypes = types(process.env.VOUCHER_TYPES || '480,0,,,;');
/**
* Output logo
*/
logo();
/**
* Log voucher types
*/
console.log('[VoucherType] Loaded the following types:');
voucherTypes.forEach((type, key) => {
console.log(`[VoucherType][${key}] ${type.expiration} minutes, ${type.usage === '1' ? 'single-use' : 'multi-use'}${typeof type.upload === "undefined" && typeof type.download === "undefined" && typeof type.megabytes === "undefined" ? ', no limits' : `${typeof type.upload !== "undefined" ? `, upload bandwidth limit: ${type.upload} kb/s` : ''}${typeof type.download !== "undefined" ? `, download bandwidth limit: ${type.download} kb/s` : ''}${typeof type.megabytes !== "undefined" ? `, quota limit: ${type.megabytes} mb` : ''}`}`);
});
/**
* Log controller
*/
console.log(`[UniFi] Using Controller on: ${process.env.UNIFI_IP || '192.168.1.1'}:${process.env.UNIFI_PORT || 443} (Site ID: ${process.env.UNIFI_SITE_ID || 'default'})`);
/**
* Trust proxy
@@ -36,7 +57,7 @@ app.use(multer().none());
* Request logger
*/
app.use((req, res, next) => {
console.log(`[Web][REQUEST]: ${req.originalUrl}`);
console.log(`[Web]: ${req.originalUrl}`);
next();
});
@@ -57,7 +78,8 @@ app.get('/', (req, res) => {
error_text: req.query.error || '',
banner_image: process.env.BANNER_IMAGE || `/images/bg-${random(1, 10)}.jpg`,
app_header: timeHeader,
sid: uuidv4()
sid: uuidv4(),
voucher_types: voucherTypes
});
});
app.post('/', async (req, res) => {
@@ -68,7 +90,7 @@ app.post('/', async (req, res) => {
return;
}
res.redirect(encodeURI(`/voucher?code=${req.body.password}`));
res.redirect(encodeURI(`/voucher?code=${req.body.password}&type=${req.body['voucher-type']}`));
});
app.get('/voucher', async (req, res) => {
if(req.query.code !== (process.env.SECURITY_CODE || "0000")) {
@@ -78,9 +100,7 @@ app.get('/voucher', async (req, res) => {
const hour = new Date().getHours();
const timeHeader = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
const voucherCode = await unifi();
console.log(voucherCode);
const voucherCode = await unifi(types(req.query.type, true));
res.render('voucher', {
error: typeof req.query.error === 'string' && req.query.error !== '',
@@ -88,6 +108,7 @@ app.get('/voucher', async (req, res) => {
banner_image: process.env.BANNER_IMAGE || `/images/bg-${random(1, 10)}.jpg`,
app_header: timeHeader,
code: req.query.code,
type: req.query.type,
voucher_code: voucherCode,
sid: uuidv4()
});
@@ -110,5 +131,5 @@ app.disable('x-powered-by');
* Start listening on port
*/
app.listen(3000, '0.0.0.0', () => {
console.log(`App is running on: 0.0.0.0:3000`);
console.log(`[App] Running on: 0.0.0.0:3000`);
});

View File

@@ -30,7 +30,9 @@
<div class="w-full flex flex-wrap">
<div class="w-full md:w-1/2 flex flex-col">
<div class="flex justify-center md:justify-start pt-12 md:pl-12 md:-mb-24">
<img class="h-20 w-20" src="/images/unifi-icon.png"/>
<a href="/" title="Homepage">
<img class="h-20 w-20" alt="UniFi Logo" src="/images/unifi-icon.png"/>
</a>
</div>
<div class="flex flex-col justify-center md:justify-start my-auto pt-8 md:pt-0 px-8 md:px-24 lg:px-32">
@@ -49,6 +51,22 @@
<input type="password" id="password" name="password" placeholder="Password" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mt-1 leading-tight focus:outline-none focus:shadow-outline dark:bg-neutral-800 dark:border-neutral-700 dark:text-gray-100" required>
</div>
<div class="flex flex-col pt-4">
<label for="voucher-type" class="text-lg">Voucher Type</label>
<div class="relative inline-block w-full">
<select id="voucher-type" name="voucher-type" class="shadow appearance-none border rounded w-full py-2 px-3 pr-8 text-gray-700 mt-1 leading-tight focus:outline-none focus:shadow-outline dark:bg-neutral-800 dark:border-neutral-700 dark:text-gray-100" required>
<% voucher_types.forEach((type) => { %>
<option value="<%= type.raw %>"><%= type.expiration %> minutes, <%= type.usage === '1' ? 'single-use' : 'multi-use' %><%= typeof type.upload === "undefined" && typeof type.download === "undefined" && typeof type.megabytes === "undefined" ? ', no limits' : '' %><%= typeof type.upload !== "undefined" ? `, upload bandwidth limit: ${type.upload} kb/s` : '' %><%= typeof type.download !== "undefined" ? `, download bandwidth limit: ${type.download} kb/s` : '' %><%= typeof type.megabytes !== "undefined" ? `, quota limit: ${type.megabytes} mb` : '' %></option>
<% }); %>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-2 mt-1 pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 fill-current" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" fillRule="evenodd"></path>
</svg>
</div>
</div>
</div>
<input type="submit" value="Create Voucher" class="bg-black text-white font-bold text-lg hover:bg-gray-700 p-2 mt-8 cursor-pointer transition-colors dark:text-black dark:bg-gray-200 dark:hover:bg-white">
</form>
</div>

View File

@@ -30,7 +30,9 @@
<div class="w-full flex flex-wrap">
<div class="w-full md:w-1/2 flex flex-col">
<div class="flex justify-center md:justify-start pt-12 md:pl-12 md:-mb-24">
<img class="h-20 w-20" src="/images/unifi-icon.png"/>
<a href="/" title="Homepage">
<img class="h-20 w-20" alt="UniFi Logo" src="/images/unifi-icon.png"/>
</a>
</div>
<div class="flex flex-col justify-center md:justify-start my-auto pt-8 md:pt-0 px-8 md:px-24 lg:px-32">
@@ -45,6 +47,7 @@
</div>
<input type="hidden" id="password" name="password" value="<%= code %>"/>
<input type="hidden" id="voucher-type" name="voucher-type" value="<%= type %>"/>
<input type="submit" value="Create Another Voucher" class="bg-black text-white font-bold text-lg hover:bg-gray-700 p-2 mt-8 cursor-pointer transition-colors dark:text-black dark:bg-gray-200 dark:hover:bg-white">
</form>
</div>