mirror of
https://github.com/glenndehaan/unifi-voucher-site.git
synced 2026-04-05 08:54:17 -04:00
Implemented API endpoints. Update README.md. Implemented health endpoint. Updated dependencies
This commit is contained in:
106
README.md
106
README.md
@@ -7,34 +7,39 @@ A small UniFi Voucher Site for simple voucher creation
|
||||

|
||||
|
||||
## Structure
|
||||
- ES6 Javascript
|
||||
|
||||
- Javascript
|
||||
- ExpressJS
|
||||
- Node UniFi
|
||||
- Tailwind
|
||||
- TailwindCSS
|
||||
|
||||
## Development Usage
|
||||
- Install NodeJS 18.0 or higher.
|
||||
|
||||
- Install NodeJS 20.0 or higher.
|
||||
- Run `npm ci` 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/
|
||||
|
||||
## Build Usage
|
||||
- Install NodeJS 18.0 or higher.
|
||||
|
||||
- Install NodeJS 20.0 or higher.
|
||||
- Run `npm ci` in the root folder
|
||||
- Run `npm run build` in the root folder
|
||||
|
||||
## 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"
|
||||
- "3000:3000"
|
||||
environment:
|
||||
# The IP address to your UniFi OS Console
|
||||
UNIFI_IP: '192.168.1.1'
|
||||
@@ -46,16 +51,103 @@ services:
|
||||
UNIFI_PASSWORD: 'password'
|
||||
# The UniFi Site ID
|
||||
UNIFI_SITE_ID: 'default'
|
||||
# The 'password' used to log in to this voucher portal
|
||||
# The 'password' used to log in to the voucher portal and used as Bearer token for the API
|
||||
SECURITY_CODE: '0000'
|
||||
# Disables the login/authentication for the portal
|
||||
# Disables the login/authentication for the portal and API
|
||||
DISABLE_AUTH: 'false'
|
||||
# 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,,,;'
|
||||
# Enable/disable the Web UI
|
||||
SERVICE_WEB: 'true'
|
||||
# Enable/disable the API
|
||||
SERVICE_API: 'false'
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
The project consists of two main services: Web and API.
|
||||
|
||||
### Web Service
|
||||
|
||||
The Web service is a user interface accessible through a web browser. It provides functionality for generating, viewing,
|
||||
and managing vouchers within the UniFi system.
|
||||
|
||||
### API Service
|
||||
|
||||
The API service allows programmatic access to voucher-related functionalities. It is designed for developers who wish to
|
||||
integrate voucher management into their applications or automate voucher generation processes. Below are the details of
|
||||
the different endpoints available in the API:
|
||||
|
||||
#### Endpoints
|
||||
|
||||
1. **`/api`**
|
||||
- Method: GET
|
||||
- Description: Retrieves information about the API and its endpoints.
|
||||
- Access: Open
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"endpoints": [
|
||||
"/api",
|
||||
"/api/types",
|
||||
"/api/voucher/:type"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **`/api/types`**
|
||||
- Method: GET
|
||||
- Description: Retrieves a list of available voucher types supported by the system.
|
||||
- Response Format: JSON
|
||||
- Access: Open
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"types": [
|
||||
{
|
||||
"expiration": "480",
|
||||
"usage": "0",
|
||||
"raw": "480,0,,,"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **`/api/voucher/:type`**
|
||||
- Method: GET
|
||||
- Description: Generates a voucher of the specified type.
|
||||
- Parameters:
|
||||
- `type` (string): The type of voucher to generate.
|
||||
- Response Format: JSON
|
||||
- Access: Protected by Bearer Token
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"voucher": "12345-67890"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> This endpoint is protected by a security mechanism. To access it, users need to include a bearer token in the
|
||||
request authorization header. The token must match the value of the `SECURITY_CODE` environment variable. Without
|
||||
this token, access to the endpoint will be denied.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8081:3000"
|
||||
- "3000:3000"
|
||||
environment:
|
||||
UNIFI_IP: '192.168.1.1'
|
||||
UNIFI_PORT: 443
|
||||
@@ -13,3 +13,5 @@ services:
|
||||
SECURITY_CODE: '0000'
|
||||
DISABLE_AUTH: 'false'
|
||||
VOUCHER_TYPES: '480,0,,,;'
|
||||
SERVICE_WEB: 'true'
|
||||
SERVICE_API: 'false'
|
||||
|
||||
@@ -6,11 +6,18 @@ const authDisabled = (process.env.DISABLE_AUTH === 'true') || false;
|
||||
/**
|
||||
* Verifies if a user is signed in
|
||||
*
|
||||
* @type {{web: ((function(*, *, *): Promise<void>)|*), api: ((function(*, *, *): Promise<void>)|*)}}
|
||||
*/
|
||||
module.exports = {
|
||||
/**
|
||||
* Handle web authentication
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
module.exports = async (req, res, next) => {
|
||||
web: async (req, res, next) => {
|
||||
// Check if authentication is enabled
|
||||
if(!authDisabled) {
|
||||
// Check if user has an existing authorization cookie
|
||||
@@ -28,4 +35,33 @@ module.exports = async (req, res, next) => {
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle api authentication
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
api: async (req, res, next) => {
|
||||
// Check if authentication is enabled
|
||||
if(!authDisabled) {
|
||||
// Check if user has sent the authorization header
|
||||
if (!req.headers.authorization) {
|
||||
res.status(401).send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if password is correct
|
||||
const passwordCheck = req.headers.authorization === `Bearer ${(process.env.SECURITY_CODE || "0000")}`;
|
||||
if (!passwordCheck) {
|
||||
res.status(403).send();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.6",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.3",
|
||||
"express": "^4.19.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-unifi": "^2.5.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
@@ -478,9 +478,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -638,16 +638,16 @@
|
||||
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.3",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
|
||||
"integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==",
|
||||
"version": "4.19.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz",
|
||||
"integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -2904,9 +2904,9 @@
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
|
||||
},
|
||||
"cookie-parser": {
|
||||
"version": "1.4.6",
|
||||
@@ -3026,16 +3026,16 @@
|
||||
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
|
||||
},
|
||||
"express": {
|
||||
"version": "4.18.3",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
|
||||
"integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==",
|
||||
"version": "4.19.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz",
|
||||
"integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.5.0",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
"build": "tailwindcss -i ./css/style.css -o ./public/dist/style.css --minify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"author": "Glenn de Haan",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.6",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.3",
|
||||
"express": "^4.19.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-unifi": "^2.5.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
|
||||
114
server.js
114
server.js
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Import base packages
|
||||
*/
|
||||
const os = require('os');
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const cookieParser = require('cookie-parser');
|
||||
@@ -30,6 +31,8 @@ const app = express();
|
||||
*/
|
||||
const random = (min, max) => Math.floor(Math.random() * (max - min)) + min;
|
||||
const voucherTypes = types(process.env.VOUCHER_TYPES || '480,0,,,;');
|
||||
const webService = (process.env.SERVICE_WEB === 'true') || true;
|
||||
const apiService = (process.env.SERVICE_API === 'true') || false;
|
||||
const authDisabled = (process.env.DISABLE_AUTH === 'true') || false;
|
||||
|
||||
/**
|
||||
@@ -37,6 +40,12 @@ const authDisabled = (process.env.DISABLE_AUTH === 'true') || false;
|
||||
*/
|
||||
logo();
|
||||
|
||||
/**
|
||||
* Log service status
|
||||
*/
|
||||
console.log(`[Service][Web] ${webService ? 'Enabled!' : 'Disabled!'}`);
|
||||
console.log(`[Service][Api] ${apiService ? 'Enabled!' : 'Disabled!'}`);
|
||||
|
||||
/**
|
||||
* Log voucher types
|
||||
*/
|
||||
@@ -48,7 +57,7 @@ voucherTypes.forEach((type, key) => {
|
||||
/**
|
||||
* Log auth status
|
||||
*/
|
||||
console.log(`[AUTH] ${authDisabled ? 'Disabled!' : 'Enabled!'}`);
|
||||
console.log(`[Auth] ${authDisabled ? 'Disabled!' : 'Enabled!'}`);
|
||||
|
||||
/**
|
||||
* Log controller
|
||||
@@ -66,6 +75,19 @@ app.enable('trust proxy');
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', `${__dirname}/template`);
|
||||
|
||||
/**
|
||||
* GET /_health - Health check page
|
||||
*/
|
||||
app.get('/_health', (req, res) => {
|
||||
res.json({
|
||||
status: 'UP',
|
||||
host: os.hostname(),
|
||||
load: process.cpuUsage(),
|
||||
mem: process.memoryUsage(),
|
||||
uptime: process.uptime()
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Enable multer
|
||||
*/
|
||||
@@ -108,11 +130,18 @@ app.use(express.static(`${__dirname}/public`));
|
||||
* Configure routers
|
||||
*/
|
||||
app.get('/', (req, res) => {
|
||||
if(webService) {
|
||||
res.redirect(302, '/voucher');
|
||||
} else {
|
||||
res.status(501).send();
|
||||
}
|
||||
});
|
||||
app.get('/login', (req, res) => {
|
||||
|
||||
// Check if web service is enabled
|
||||
if(webService) {
|
||||
app.get('/login', (req, res) => {
|
||||
// Check if authentication is disabled
|
||||
if(authDisabled) {
|
||||
if (authDisabled) {
|
||||
res.redirect(302, '/voucher');
|
||||
return;
|
||||
}
|
||||
@@ -127,9 +156,9 @@ app.get('/login', (req, res) => {
|
||||
app_header: timeHeader,
|
||||
sid: req.sid
|
||||
});
|
||||
});
|
||||
app.post('/login', async (req, res) => {
|
||||
if(typeof req.body === "undefined") {
|
||||
});
|
||||
app.post('/login', async (req, res) => {
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
@@ -142,8 +171,8 @@ app.post('/login', async (req, res) => {
|
||||
}
|
||||
|
||||
res.cookie('authorization', req.body.password, {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, '/voucher');
|
||||
});
|
||||
app.get('/voucher', [authorization], async (req, res) => {
|
||||
});
|
||||
app.get('/voucher', [authorization.web], async (req, res) => {
|
||||
const hour = new Date().getHours();
|
||||
const timeHeader = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
|
||||
|
||||
@@ -159,9 +188,9 @@ app.get('/voucher', [authorization], async (req, res) => {
|
||||
voucher_types: voucherTypes,
|
||||
vouchers_popup: false
|
||||
});
|
||||
});
|
||||
app.post('/voucher', [authorization], async (req, res) => {
|
||||
if(typeof req.body === "undefined") {
|
||||
});
|
||||
app.post('/voucher', [authorization.web], async (req, res) => {
|
||||
if (typeof req.body === "undefined") {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
@@ -181,8 +210,8 @@ app.post('/voucher', [authorization], async (req, res) => {
|
||||
if(voucherCode) {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'info', message: `Voucher Created: ${voucherCode}`}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, '/voucher');
|
||||
}
|
||||
});
|
||||
app.get('/vouchers', [authorization], async (req, res) => {
|
||||
});
|
||||
app.get('/vouchers', [authorization.web], async (req, res) => {
|
||||
const hour = new Date().getHours();
|
||||
const timeHeader = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
|
||||
|
||||
@@ -190,7 +219,7 @@ app.get('/vouchers', [authorization], async (req, res) => {
|
||||
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, '/voucher');
|
||||
});
|
||||
|
||||
if(vouchers) {
|
||||
if (vouchers) {
|
||||
res.render('voucher', {
|
||||
info: req.flashMessage.type === 'info',
|
||||
info_text: req.flashMessage.message || '',
|
||||
@@ -205,7 +234,62 @@ app.get('/vouchers', [authorization], async (req, res) => {
|
||||
vouchers
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(apiService) {
|
||||
app.get('/api', (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
endpoints: [
|
||||
'/api',
|
||||
'/api/types',
|
||||
'/api/voucher/:type'
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
app.get('/api/types', (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
types: voucherTypes
|
||||
}
|
||||
});
|
||||
});
|
||||
app.get('/api/voucher/:type', [authorization.api], async (req, res) => {
|
||||
const typeCheck = (process.env.VOUCHER_TYPES || '480,0,,,;').split(';').includes(req.params.type);
|
||||
|
||||
if(!typeCheck) {
|
||||
res.json({
|
||||
error: 'Unknown Type!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi(types(req.params.type, true)).catch((e) => {
|
||||
res.json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
if(voucherCode) {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: voucherCode
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup default 404 message
|
||||
|
||||
Reference in New Issue
Block a user