Implemented API endpoints. Update README.md. Implemented health endpoint. Updated dependencies

This commit is contained in:
Glenn de Haan
2024-03-22 17:13:37 +01:00
parent 611e8cc45c
commit 95989c212c
6 changed files with 338 additions and 124 deletions

106
README.md
View File

@@ -7,34 +7,39 @@ A small UniFi Voucher Site for simple voucher creation
![Screenshot](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/64f199e6-5e50-4d91-8731-1f970c1f1210)
## 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

View File

@@ -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'

View File

@@ -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
View File

@@ -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",

View File

@@ -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
View File

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