Mayor V2 refactor. Now also available on DockerHub

This commit is contained in:
Glenn de Haan
2021-01-16 09:45:22 +01:00
parent 0e2053846a
commit 22162db21a
35 changed files with 4720 additions and 6962 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
# PhpStorm
.idea
# General files
*~
*.DS_STORE
# Dependency managers
node_modules/
npm-debug.log
# Build files
public/dist/

View File

@@ -8,11 +8,6 @@ end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# PHP PSR2 overrides
[*.php]
indent_style = space
indent_size = 4
# JS overrides
[*.js]
indent_style = space

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
liberapay: glenndehaan

5
.gitignore vendored
View File

@@ -10,7 +10,4 @@ node_modules/
npm-debug.log
# Build files
dist/
# Project
app/config/config.js
public/dist/

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM alpine:3.13
# Install packages
RUN apk add --no-cache nginx nodejs npm
# Create app directory
WORKDIR /usr/src/app
# Bundle app source
COPY . .
# Create production build
RUN npm ci --only=production && npm run build
# Expose app
EXPOSE 3001
# Run app
CMD ["node", "/usr/src/app/app/server.js"]

View File

@@ -1,14 +1,30 @@
# UniFi Voucher Site
## Usage
A small UniFi Voucher Site for simple voucher creation
[![Build Status](https://img.shields.io/docker/cloud/build/glenndehaan/unifi-voucher-site.svg)](https://hub.docker.com/r/glenndehaan/unifi-voucher-site) [![Build Status](https://img.shields.io/docker/cloud/automated/glenndehaan/unifi-voucher-site.svg)](https://hub.docker.com/r/glenndehaan/unifi-voucher-site)
## Structure
- ES6 Javascript
- SCSS
- Node UniFi
- Webpack
## Development Usage
- Install NodeJS 8.0 or higher.
- Copy the `config.sample.js` to `config.js` in the `app/config` folder
- Change the `config.js` to your liking's and add your UniFi controller credentials
- Run `npm install` in the root folder
- Run `npm ci` in the root folder
- Run `npm start` in the root folder
Then open up your favorite web browser and go to: http://localhost:3001
That's it. Easy right?
Then open up your favorite browser and go to http://localhost:3001/
## Build Usage
- Install NodeJS 8.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`
## License

View File

@@ -1,19 +1,16 @@
const webpack = require('webpack');
const path = require('path');
const autoprefixer = require('autoprefixer');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const browsers = require('../package.json').browserslist;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
//Get path so every environment works
const projectPath = path.resolve(__dirname);
//Environment check depending on call from
const env = process.env.NODE_ENV || 'development';
const isProd = env === 'production';
//Define all the global config for both production & dev
const config = {
module.exports = {
entry: {
main: [
projectPath + '/../public/js/main.js',
@@ -22,37 +19,30 @@ const config = {
},
output: {
path: projectPath + '/../public/dist/',
filename: '[name].[hash:6].js'
filename: '[name].[fullhash:6].js',
publicPath: ''
},
module: {
loaders: [
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['babel-preset-es2015'].map(require.resolve),
sourceMaps: 'inline'
use: {
loader: 'babel-loader',
options: {
presets: [
require.resolve('@babel/preset-env')
]
}
}
},
{
test: /\.(css|scss)$/,
loader: ExtractTextPlugin.extract({
use: [
'raw-loader?url=false', {
loader: 'postcss-loader',
options: {
ident: 'postcss',
context: path.resolve(__dirname, '../public'),
sourceMap: 'inline',
plugins: [
autoprefixer({browsers})
]
}
},
'sass-loader?sourceMap=true'
]
})
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader?url=false',
'sass-loader'
]
}
]
},
@@ -62,8 +52,10 @@ const config = {
NODE_ENV: JSON.stringify(env)
}
}),
new ExtractTextPlugin({filename: 'main.[hash:6].css', allChunks: true}),
new ManifestPlugin({
new MiniCssExtractPlugin({
filename: '[name].[fullhash:6].css'
}),
new WebpackManifestPlugin({
fileName: 'rev-manifest.json'
})
],
@@ -72,33 +64,3 @@ const config = {
modules: [path.join(__dirname, '../node_modules')]
}
};
//Extra options depending on environment
if (isProd) {
Object.assign(config, {
plugins: config.plugins.concat([
new webpack.LoaderOptionsPlugin({
minimize: true
}),
new webpack.optimize.UglifyJsPlugin({
drop_console: true,
output: {
comments: false
},
compressor: {
screw_ie8: true,
warnings: false
}
})
])
});
} else {
Object.assign(config, {
devtool: 'cheap-module-eval-source-map',
plugins: config.plugins.concat([
new webpack.NoEmitOnErrorsPlugin()
])
});
}
module.exports = config;

22
app/config/config.js Normal file
View File

@@ -0,0 +1,22 @@
/**
* General config
*/
module.exports = {
application: {
name: "UniFi Voucher",
env: " (local)",
basePath: "/",
port: 3001,
bind: "0.0.0.0"
},
security: {
code: process.env.SECURITY_CODE || "0000" // <- Only 4 digits
},
unifi: {
ip: process.env.UNIFI_IP || '192.168.1.1',
port: process.env.UNIFI_PORT || 8443,
username: process.env.UNIFI_USERNAME || 'admin',
password: process.env.UNIFI_PASSWORD || 'password',
siteID: process.env.UNIFI_SITE_ID || 'default'
}
};

View File

@@ -1,34 +0,0 @@
/**
* General config
*/
const config = {
application: {
name: "UniFi Voucher",
env: " (local)",
basePath: "/",
port: 3001,
bind: "0.0.0.0",
supportedBrowsers: [
"Chrome >= 52",
"Firefox >= 47",
"Safari >= 10",
"Edge == All",
"IE == 11"
]
},
session: {
secret: "averysecretstring"
},
security: {
code: "0000" // <- Only 4 digits
},
unifi: {
ip: '192.168.1.XX',
port: 8443,
username: 'admin',
password: 'XXXXXXXXXXXXXXXXXXX',
siteID: 'default'
}
};
module.exports = config;

View File

@@ -1,21 +0,0 @@
class BaseController {
constructor() {
}
/**
* Send a response to express
*
* @param response
* @param status
* @param data
* @param contentType
*/
jsonResponse(response, status, data, contentType = 'application/json') {
response.type(contentType);
response.status(status);
response.json(data);
}
}
module.exports = BaseController;

View File

@@ -1,19 +0,0 @@
const baseController = require('./BaseController');
class IndexController extends baseController {
constructor() {
super();
}
/**
* Action for the default api route
*
* @param req
* @param res
*/
indexAction(req, res) {
this.jsonResponse(res, 200, { 'message': 'Default API route!' });
}
}
module.exports = new IndexController();

View File

@@ -1,5 +1,5 @@
const config = require("../../config/config");
const assets = require("../../helpers/modules/Assets");
const config = require("../config/config");
const assets = require("../modules/Assets");
class BaseController {
constructor() {

View File

@@ -1,6 +0,0 @@
const fs = require("fs");
const path = `${__dirname}/../../../public/dist`;
module.exports = () => {
return JSON.parse(fs.existsSync(path) ? fs.readFileSync(`${path}/rev-manifest.json`) : "{}");
};

View File

@@ -1,26 +0,0 @@
class Router {
/**
* An easy to use function to add multiple routes to the Express router
*
* @param router
* @param routes
* @param type
*/
routesToRouter(router, routes, type) {
for (let item = 0; item < routes.length; item += 1) {
const route = routes[item];
const controller = route.controller.charAt(0).toUpperCase() + route.controller.slice(1);
eval(
`
const ${route.controller}Controller = require('../../controllers/${type}/${controller}Controller');
router.${route.method}('${route.route}', (req, res) => {
${route.controller}Controller.${route.action}Action(req, res);
});
`
);
}
}
}
module.exports = new Router();

18
app/modules/Assets.js Normal file
View File

@@ -0,0 +1,18 @@
/**
* Import vendor modules
*/
const fs = require("fs");
/**
* Define public path
*/
const path = `${__dirname}/../../public/dist`;
/**
* Return the manifest
*
* @return {any}
*/
module.exports = () => {
return JSON.parse(fs.existsSync(path) ? fs.readFileSync(`${path}/rev-manifest.json`) : "{}");
};

View File

@@ -1,8 +1,23 @@
const uuidv4 = require('uuid/v4');
const config = require('../../config/config');
/**
* Import vendor packages
*/
const { v4: uuidv4 } = require('uuid');
/**
* Import own packages
*/
const config = require('../config/config');
const unifi = require('./UniFi');
/**
* Socket
*/
class Socket {
/**
* Constructor
*
* @param server
*/
constructor(server) {
this.io = require('socket.io')(server);
this.authenticatedUsers = [];
@@ -125,4 +140,9 @@ class Socket {
}
}
/**
* Export the socket class
*
* @type {Socket}
*/
module.exports = Socket;

View File

@@ -1,5 +1,12 @@
/**
* Import vendor modules
*/
const unifi = require('node-unifi');
const config = require('../../config/config');
/**
* Import own modules
*/
const config = require('../config/config');
/**
* Create new UniFi controller object
@@ -8,6 +15,11 @@ const config = require('../../config/config');
*/
const controller = new unifi.Controller(config.unifi.ip, config.unifi.port);
/**
* Exports the UniFi voucher function
*
* @param callback
*/
module.exports = (callback) => {
controller.login(config.unifi.username, config.unifi.password, (err) => {
if(err) {

View File

@@ -1,22 +0,0 @@
/**
* Import base packages
*/
const express = require('express');
const router = express.Router();
const routerUtils = require('../helpers/modules/Router');
/**
* Define routes
*/
const routes = [
{
route: '/',
method: 'get',
controller: 'Index',
action: 'index'
}
];
routerUtils.routesToRouter(router, routes, 'Api');
module.exports = {router, routes};

View File

@@ -1,22 +0,0 @@
/**
* Import base packages
*/
const express = require('express');
const router = express.Router();
const routerUtils = require('../helpers/modules/Router');
/**
* Define routes
*/
const routes = [
{
route: '/',
method: 'get',
controller: 'Index',
action: 'index'
}
];
routerUtils.routesToRouter(router, routes, 'Web');
module.exports = {router, routes};

View File

@@ -3,18 +3,13 @@
*/
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const session = require('express-session');
const browsersupport = require('express-browsersupport');
/**
* Import own packages
*/
const config = require('./config/config');
const socket = require('./helpers/modules/Socket');
const webRouter = require('./routers/Web');
const apiRouter = require('./routers/Api');
const indexController = require('./controllers/Web/IndexController');
const socket = require('./modules/Socket');
const IndexController = require('./controllers/IndexController');
/**
* Set template engine
@@ -33,64 +28,10 @@ app.enable('trust proxy');
app.use(express.static(`${__dirname}/../public`));
/**
* Configure app to use bodyParser()
* Configure routes
*/
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
/**
* Configure sessions in app
*/
app.use(session({secret: config.session.secret, resave: true, saveUninitialized: true}));
/**
* Configure app to use Browser Support
*/
app.use(browsersupport({
redirectUrl: "/oldbrowser",
supportedBrowsers: config.application.supportedBrowsers
}));
/**
* Configure routers
*/
app.use('/', webRouter.router);
app.use('/api', apiRouter.router);
/**
* Render sitemap.xml and robots.txt
*/
app.get('/sitemap.xml', (req, res) => {
indexController.siteMapAction(req, res, webRouter.routes);
});
app.get('/robots.txt', (req, res) => {
indexController.robotsAction(req, res);
});
/**
* Render old browser page
*/
app.get('/oldbrowser', (req, res) => {
indexController.oldBrowserAction(req, res);
});
/**
* Setup default 404 message
*/
app.use((req, res) => {
res.status(404);
// respond with json
if (req.originalUrl.split('/')[1] === 'api') {
/**
* API 404 not found
*/
res.send({error: 'This API route is not implemented yet'});
return;
}
indexController.notFoundAction(req, res);
app.get('/', (req, res) => {
IndexController.indexAction(req, res);
});
/**

View File

@@ -1 +0,0 @@
<h2>404 Not Found !</h2>

View File

@@ -1,4 +0,0 @@
<h2>Old Browser</h2>
<p>
Please update your browser to make sure our website works properly
</p>

View File

@@ -1,4 +0,0 @@
User-agent: *
Disallow: /fonts/
Disallow: /scss/
Sitemap: <%= baseUrl %>sitemap.xml

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc><%= baseUrl %></loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<% routes.forEach((route) => { %>
<% if(route.route !== "/") { %>
<url>
<loc><%= baseUrl %><%= route.route.substring(1) %></loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<% } %>
<% }); %>
</urlset>

View File

@@ -1,16 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<% include ./partials/head.ejs %>
<%- include('partials/head') %>
</head>
<body>
<% include ./partials/preloader.ejs %>
<%- include('partials/preloader') %>
<main>
<%- include(template) %>
</main>
<% include ./partials/footer.ejs %>
<%- include('partials/footer') %>
<script>
var expressConfig = {protocol: '<%= protocol %>', hostname: '<%= hostname %>', baseUrl: '<%= baseUrl %>'};
</script>

View File

@@ -1,7 +1,7 @@
<div id="particles"></div>
<div id="container">
<div id="center">
<img src="/images/unifi-icon.png" alt="UniFi Icon" /><br/>
<img src="/images/unifi-icon.png" alt="UniFi Icon" width="100px" height="100px" /><br/>
<h2><%= config.application.name %></h2>
<div id="sign-in">

View File

@@ -1,3 +1,3 @@
<footer>
<p>© Copyright The Awesome People</p>
<p>UniFi Voucher | <a href="https://github.com/glenndehaan/unifi-voucher-site" rel="noreferrer noopener">GitHub</a></p>
</footer>

View File

@@ -3,14 +3,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="description" content="">
<meta name="description" content="UniFi Voucher">
<meta name="author" content="Glenn de Haan">
<meta name="keywords" content="">
<meta property="og:title" content="<%= config.application.name %>"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="<%= baseUrl %>"/>
<meta property="og:description" content=""/>
<meta property="og:description" content="UniFi Voucher"/>
<link rel="alternate" href="<%= baseUrl %>" hreflang="en"/>
<link rel="canonical" href="<%= baseUrl %>"/>
@@ -79,3 +78,4 @@
</style>
<link href="/dist/<%= assets.css %>" rel="stylesheet" type="text/css"/>
<link rel="preload" href="/fonts/text-security-disc.woff" as="font">

13
docker-compose.yml Normal file
View File

@@ -0,0 +1,13 @@
version: '3'
services:
app:
build: .
ports:
- "8081:3001"
environment:
UNIFI_IP: '192.168.1.1'
UNIFI_PORT: 8443
UNIFI_USERNAME: 'admin'
UNIFI_PASSWORD: 'password'
UNIFI_SITE_ID: 'default'
SECURITY_CODE: '0000'

10966
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,9 @@
"backend:start": "node ./app/server.js",
"backend:dev": "nodemon -L --watch ./app ./app/server.js",
"prebuild": "rimraf ./public/dist",
"build": "cross-env NODE_ENV=production webpack --config ./_scripts/webpack.config.js --progress",
"build": "cross-env NODE_ENV=production webpack --mode production --config ./_scripts/webpack.config.js",
"prefrontend": "rimraf ./public/dist",
"frontend": "cross-env NODE_ENV=development webpack --config ./_scripts/webpack.config.js --watch --watch-poll -d --progress",
"frontend": "webpack --watch --mode development --config ./_scripts/webpack.config.js",
"lint": "eslint -c ./package.json ./app ./public/js"
},
"engines": {
@@ -38,9 +38,7 @@
},
"globals": {
"site": false,
"expressConfig": false,
"TimelineMax": false,
"TweenMax": false
"expressConfig": false
},
"extends": [
"eslint:recommended"
@@ -49,36 +47,33 @@
"author": "Glenn de Haan",
"license": "MIT",
"dependencies": {
"autoprefixer": "^8.0.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.3",
"babel-preset-es2015": "^6.24.1",
"body-parser": "^1.17.2",
"cross-env": "^5.1.3",
"ejs": "^2.5.6",
"express": "^4.15.3",
"express-browsersupport": "^1.3.3",
"express-session": "^1.15.6",
"extract-text-webpack-plugin": "^3.0.2",
"gsap": "^1.20.4",
"mitt": "^1.1.3",
"node-sass": "^4.7.2",
"node-unifi": "^1.2.2",
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"animejs": "^3.2.1",
"babel-core": "^6.26.3",
"babel-loader": "^8.2.2",
"cross-env": "^7.0.3",
"css-loader": "^5.0.1",
"ejs": "^3.1.5",
"express": "^4.17.1",
"gsap": "^3.6.0",
"mini-css-extract-plugin": "^1.3.4",
"mitt": "^2.1.0",
"node-sass": "^5.0.0",
"node-unifi": "^1.3.8",
"particles.js": "^2.0.0",
"postcss": "^6.0.19",
"postcss-loader": "^2.1.1",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.2",
"sass-loader": "^6.0.6",
"socket.io": "^2.0.4",
"socket.io-client": "^2.0.4",
"uuid": "^3.2.1",
"webpack": "^3.11.0",
"webpack-manifest-plugin": "^1.3.2"
"rimraf": "^3.0.2",
"sass-loader": "^10.1.1",
"socket.io": "^3.1.0",
"socket.io-client": "^3.1.0",
"uuid": "^8.3.2",
"webpack": "^5.14.0",
"webpack-cli": "^4.3.1",
"webpack-manifest-plugin": "^3.0.0"
},
"devDependencies": {
"concurrently": "^3.5.1",
"eslint": "^4.18.1",
"nodemon": "^1.15.1"
"concurrently": "^5.3.0",
"eslint": "^7.17.0",
"nodemon": "^2.0.7"
}
}

View File

@@ -1,6 +1,4 @@
import mitt from 'mitt';
import 'gsap/TweenMax';
import 'gsap/TimelineMax';
import settings from './settings';
/**

View File

@@ -1,4 +1,5 @@
import io from 'socket.io-client';
import animejs from 'animejs';
export default class Socket {
constructor({el}) {
@@ -13,8 +14,6 @@ export default class Socket {
this.errorContainer = document.querySelector("#error");
this.preloader = document.querySelector("#preloader");
this.tl = new TimelineMax();
document.querySelector("#voucher button").addEventListener("click", () => this.requestVoucher());
this.init();
@@ -46,12 +45,12 @@ export default class Socket {
this.socket.emit('uuid');
}
this.tl
.to(this.mainContainer, 0.5, {
opacity: 0,
display: "none"
})
.add(() => {
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [1, 0],
easing: 'linear',
complete: () => {
if (!this.userSignedIn) {
this.signInContainer.classList.remove("hidden");
} else {
@@ -59,11 +58,15 @@ export default class Socket {
}
this.errorContainer.classList.add("hidden");
})
.to(this.mainContainer, 0.5, {
opacity: 1,
display: "block"
});
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [0, 1],
easing: 'linear',
});
}
});
}
/**
@@ -72,20 +75,24 @@ export default class Socket {
disconnect() {
console.log('[SOCKET] Disconnected!');
this.tl
.to(this.mainContainer, 0.5, {
opacity: 0,
display: "none"
})
.add(() => {
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [1, 0],
easing: 'linear',
complete: () => {
this.signInContainer.classList.add("hidden");
this.voucherContainer.classList.add("hidden");
this.errorContainer.classList.remove("hidden");
})
.to(this.mainContainer, 0.5, {
opacity: 1,
display: "block"
});
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [0, 1],
easing: 'linear'
});
}
});
}
/**
@@ -94,20 +101,24 @@ export default class Socket {
error() {
console.log('[SOCKET] Error!');
this.tl
.to(this.mainContainer, 0.5, {
opacity: 0,
display: "none"
})
.add(() => {
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [1, 0],
easing: 'linear',
complete: () => {
this.signInContainer.classList.add("hidden");
this.voucherContainer.classList.add("hidden");
this.errorContainer.classList.remove("hidden");
})
.to(this.mainContainer, 0.5, {
opacity: 1,
display: "block"
});
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [0, 1],
easing: 'linear'
});
}
});
}
/**
@@ -124,20 +135,24 @@ export default class Socket {
if (data.success) {
this.userSignedIn = true;
this.tl
.to(this.mainContainer, 0.5, {
opacity: 0,
display: "none"
})
.add(() => {
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [1, 0],
easing: 'linear',
complete: () => {
site.modules.Signin.resetForm();
this.signInContainer.classList.add("hidden");
this.voucherContainer.classList.remove("hidden");
})
.to(this.mainContainer, 0.5, {
opacity: 1,
display: "block"
});
animejs({
targets: this.mainContainer,
duration: 250,
opacity: [0, 1],
easing: 'linear'
});
}
});
} else {
site.modules.Signin.invalidCode();
}

View File

@@ -1,33 +0,0 @@
/**
* Function to post data to the API
*
* @param method
* @param data
* @param callback
* @param auth
*/
export default (method, data, callback, auth = "") => {
const xmlhttp = new XMLHttpRequest();
let route = "/api";
xmlhttp.open("POST", route);
xmlhttp.setRequestHeader("Content-Type", "application/json");
xmlhttp.setRequestHeader("Bearer", auth);
xmlhttp.send(JSON.stringify(data));
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === XMLHttpRequest.DONE) {
const responseCode = xmlhttp.status;
const response = JSON.parse(xmlhttp.responseText);
console.log('response', response);
console.log('responseCode', responseCode);
if (responseCode === 200) {
callback({error: false});
} else {
callback({error: true, fields: response.fields, raw_error: response.error});
}
}
}
}