This commit is contained in:
William Mantly 2021-06-25 12:02:37 -04:00
parent 7d562ba010
commit 1f96cbb23e
Signed by: wmantly
GPG Key ID: 186A8370EFF937CA
17 changed files with 620 additions and 145 deletions

View File

@ -7,12 +7,32 @@ const express = require('express');
// Set up the express app. // Set up the express app.
const app = express(); const app = express();
// Hold list of functions to run when the server is ready
app.onListen = [function(){console.log('hello')}];
// Allow the express app to be exported into other files. // Allow the express app to be exported into other files.
module.exports = app; module.exports = app;
// Build the conf object from the conf files. // Build the conf object from the conf files.
app.conf = require('./conf/conf'); app.conf = require('./conf/conf');
// Grab the projects PubSub
app.ps = require('./controller/pubsub.js');
// Push pubsub over the socket and back.
app.onListen.push(function(){
app.ps.subscribe(/./g, function(data, topic){
app.io.emit('P2PSub', { topic, data });
});
app.io.on('connection', (socket) => {
socket.on('P2PSub', (msg) => {
app.ps.publish(msg.topic, msg.data);
socket.broadcast.emit('P2PSub', msg);
});
});
});
// Hold onto the auth middleware // Hold onto the auth middleware
const middleware = require('./middleware/auth'); const middleware = require('./middleware/auth');
@ -38,6 +58,7 @@ app.use('/api/git', require('./routes/git_webhook.js'));
// API routes for working with users. All endpoints need to be have valid user. // API routes for working with users. All endpoints need to be have valid user.
app.use('/api/user', middleware.auth, require('./routes/user')); app.use('/api/user', middleware.auth, require('./routes/user'));
app.use('/api/repo', middleware.auth, require('./routes/repo'));

View File

@ -22,6 +22,9 @@ app.set('port', port);
var server = http.createServer(app); var server = http.createServer(app);
var io = require('socket.io')(server);
app.io = io;
/** /**
* Listen on provided port, on all network interfaces. * Listen on provided port, on all network interfaces.
*/ */
@ -87,5 +90,9 @@ function onListening() {
var bind = typeof addr === 'string' var bind = typeof addr === 'string'
? 'pipe ' + addr ? 'pipe ' + addr
: 'port ' + addr.port; : 'port ' + addr.port;
debug('Listening on ' + bind); console.log('Listening on ' + bind);
for(let listener of app.onListen){
listener()
}
} }

View File

@ -0,0 +1,7 @@
const {PubSub} = require('p2psub');
ps = new PubSub();
console.log(ps.subscribe())
module.exports = ps;

View File

@ -3,7 +3,7 @@ const extend = require('extend');
const axios = require('axios') const axios = require('axios')
const {Repo, Environment, Deployment, Target} = require('../models/repo'); const {Repo, Environment, Deployment, Target} = require('../models/repo');
const deployTargets = require('./lxc'); const deployTargets = require('./lxc');
const conf = require('../conf/conf') const conf = require('../conf/conf');
async function doDeploy(action, repo, branch, repoSshurl, commit){ async function doDeploy(action, repo, branch, repoSshurl, commit){
var deployment; var deployment;
@ -83,6 +83,7 @@ class Depoy{
} }
async event(name, data){ async event(name, data){
console.log(`EVENT: ${name}`, data) console.log(`EVENT: ${name}`, data)
} }
@ -137,6 +138,10 @@ class Depoy{
async update(){ async update(){
await this.exec(` await this.exec(`
cd ${this.settings.workingPath}; cd ${this.settings.workingPath};
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
git stash
export GIT_SSH_COMMAND="/usr/bin/ssh -o StrictHostKeyChecking=no -i $HOME/.ssh/id_rsa_deploy_key" export GIT_SSH_COMMAND="/usr/bin/ssh -o StrictHostKeyChecking=no -i $HOME/.ssh/id_rsa_deploy_key"
git pull origin master; git pull origin master;
./${this.settings.scriptsPath}/${this.environment.environment}/update.sh ./${this.settings.scriptsPath}/${this.environment.environment}/update.sh
@ -237,16 +242,18 @@ module.exports = {doDeploy};
// for(let d of deployments){ // for(let d of deployments){
// try{ // try{
// let lxc = new deployTargets.LXC({...{name: d.repo_env_id.replace('/', '_')}, ...d.target.settings}) // let lxc = new deployTargets.LXC({...{name: d.repo_env_id.replace('/', '_')}, ...d.target.settings})
// await lxc.destroy(); // console.log('deployment', d)
// await d.remove() // // await lxc.destroy();
// console.log(await d.remove());
// }catch(error){ // }catch(error){
// console.log('err', error) // console.log('err', error)
// }finally{ // }finally{
// await d.remove() // await d.remove();
// } // }
// } // }
}catch(error){ }catch(error){
console.error('IIFE error:', error) console.error('IIFE error:', error)
}})() }})()

View File

@ -5,7 +5,6 @@ const {Auth} = require('../models/auth');
async function auth(req, res, next){ async function auth(req, res, next){
try{ try{
let user = await Auth.checkToken({token: req.header('auth-token')}); let user = await Auth.checkToken({token: req.header('auth-token')});
if(user.uid){ if(user.uid){
req.user = user; req.user = user;
return next(); return next();

View File

@ -35,6 +35,7 @@ Auth.checkToken = async function(data){
return await User.get(token.created_by); return await User.get(token.created_by);
} }
}catch(error){ }catch(error){
console.log('token error', data)
throw this.errors.login(); throw this.errors.login();
} }
}; };

View File

@ -109,7 +109,6 @@ class Environment extends Table{
'lastCommit': {default:"__NONE__", isRequired: false, type: 'string'}, 'lastCommit': {default:"__NONE__", isRequired: false, type: 'string'},
'workingPath': {default: '/opt/datacom', type: 'string'}, 'workingPath': {default: '/opt/datacom', type: 'string'},
'domain': {isRequired: true, type: 'string'}, 'domain': {isRequired: true, type: 'string'},
} }
static async add(data){ static async add(data){
@ -183,30 +182,30 @@ module.exports = {Repo, Environment, Deployment, Target};
// // To ssh://git.theta42.com:2222/wmantly/static-test.git // // To ssh://git.theta42.com:2222/wmantly/static-test.git
let lxc_starting = await Target.add({ // let lxc_staging = await Target.add({
created_by: 'wmantly', // created_by: 'wmantly',
name: 'lxc_starting', // name: 'lxc_staging',
type: 'LXC', // type: 'LXC',
settings: { // settings: {
user:'virt-service', // user:'virt-service',
host:'lxc-staging0.sfo2.do.datacominfra.net', // host:'lxc-staging0.sfo2.do.datacominfra.net',
keyPath:'/home/william/.ssh/id_rsa_virt-service' // keyPath:'/home/william/.ssh/id_rsa_virt-service'
} // }
}); // });
var repo = await Repo.add({ // var repo = await Repo.add({
created_by: 'wmantly', // created_by: 'wmantly',
repo: 'wmantly/static-test', // repo: 'wmantly/static-test',
}) // })
var environment = await Environment.add({ // var environment = await Environment.add({
created_by: 'wmantly', // created_by: 'wmantly',
environment: 'staging', // environment: 'staging',
branchMatch: '*', // branchMatch: '*',
repo: 'wmantly/static-test', // repo: 'wmantly/static-test',
domain: '*.dc.vm42.us', // domain: '*.dc.vm42.us',
target: 'lxc_starting' // target: 'lxc_staging'
}) // })
@ -214,20 +213,20 @@ module.exports = {Repo, Environment, Deployment, Target};
// await environment.update({'domain': '*.dc.vm42.us'}) // await environment.update({'domain': '*.dc.vm42.us'})
// // console.log(test) // console.log(test)
// // console.log(await Environment.listDetail()) // console.log(await Environment.listDetail())
// // let repo = await Repo.get('wmantly/test2') // let repo = await Repo.get('wmantly/static-test')
// // console.log(repo) // console.log(repo)
// // repo.update({hookCallCount: 5}); // repo.update({hookCallCount: 5});
// // let envs = await repo.getEnvironments(); // let envs = await repo.getEnvironments();
// // let env = await repo.getEnvironmentsbyBranch('staging'); // let env = await repo.getEnvironmentsbyBranch('staging');
// // let deployment = await env.addDeployment() // let deployment = await env.addDeployment()
// // console.log('deployment', deployment) // console.log('deployment', deployment)
// // let deployments = await repo.getDeploymentsbyBranch('staging') // let deployments = await repo.getDeploymentsbyBranch('staging')
// // console.log('deployments', deployments) // console.log('deployments', deployments)
// // console.log('deployments', await Deployment.listDetail()) // console.log('deployments', await Deployment.listDetail())
@ -251,6 +250,7 @@ module.exports = {Repo, Environment, Deployment, Target};
// console.log('deployments', deployment) // console.log('deployments', deployment)
// console.log(await Repo.listDetail())
// return 0; // return 0;
}catch(error){ }catch(error){

380
nodejs/package-lock.json generated
View File

@ -17,7 +17,9 @@
"moment": "^2.29.1", "moment": "^2.29.1",
"mustache": "^4.1.0", "mustache": "^4.1.0",
"node-forge": "^0.10.0", "node-forge": "^0.10.0",
"redis": "^2.8.0" "p2psub": "^0.1.9",
"redis": "^2.8.0",
"socket.io": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^2.0.6" "nodemon": "^2.0.6"
@ -52,6 +54,21 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/component-emitter": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
},
"node_modules/@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg=="
},
"node_modules/@types/cors": {
"version": "2.8.10",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "14.14.22", "version": "14.14.22",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz",
@ -203,6 +220,22 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
}, },
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -422,7 +455,6 @@
"dependencies": { "dependencies": {
"anymatch": "~3.1.1", "anymatch": "~3.1.1",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0", "glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
@ -476,6 +508,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}, },
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -527,6 +564,18 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
}, },
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -649,6 +698,63 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/engine.io": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz",
"integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.0",
"ws": "~7.4.2"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
"dependencies": {
"base64-arraybuffer": "0.1.4"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/escape-goat": { "node_modules/escape-goat": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
@ -1389,6 +1495,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@ -1418,6 +1532,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/p2psub": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/p2psub/-/p2psub-0.1.9.tgz",
"integrity": "sha512-5za6YUq6GLH+iZCqOF7YaOd13joJyqJyjp/6o3wcy9hj969EJq7sJGsU5b3MB7tjMoaHAaIKZ01j99Y9QficUQ=="
},
"node_modules/package-json": { "node_modules/package-json": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
@ -1720,6 +1839,85 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true "dev": true
}, },
"node_modules/socket.io": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.0.tgz",
"integrity": "sha512-/c1riZMV/4yz7KEpaMhDQbwhJDIoO55whXaRKgyEBQrLU9zUHXo9rzeTMvTOqwL9mbKfHKdrXcMoCeQ/1YtMsg==",
"dependencies": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.1",
"engine.io": "~5.0.0",
"socket.io-adapter": "~2.2.0",
"socket.io-parser": "~4.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
"integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
},
"node_modules/socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"dependencies": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
@ -2043,6 +2241,26 @@
"typedarray-to-buffer": "^3.1.5" "typedarray-to-buffer": "^3.1.5"
} }
}, },
"node_modules/ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xdg-basedir": { "node_modules/xdg-basedir": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
@ -2077,6 +2295,21 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/component-emitter": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
},
"@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg=="
},
"@types/cors": {
"version": "2.8.10",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
},
"@types/node": { "@types/node": {
"version": "14.14.22", "version": "14.14.22",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz",
@ -2206,6 +2439,16 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
}, },
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
},
"base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"binary-extensions": { "binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -2417,6 +2660,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}, },
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2456,6 +2704,15 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
}, },
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"crypto-random-string": { "crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -2554,6 +2811,48 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"engine.io": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz",
"integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==",
"requires": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.0",
"ws": "~7.4.2"
},
"dependencies": {
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
},
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"engine.io-parser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
"requires": {
"base64-arraybuffer": "0.1.4"
}
},
"escape-goat": { "escape-goat": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
@ -3098,6 +3397,11 @@
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
"dev": true "dev": true
}, },
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"on-finished": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@ -3121,6 +3425,11 @@
"integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
"dev": true "dev": true
}, },
"p2psub": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/p2psub/-/p2psub-0.1.9.tgz",
"integrity": "sha512-5za6YUq6GLH+iZCqOF7YaOd13joJyqJyjp/6o3wcy9hj969EJq7sJGsU5b3MB7tjMoaHAaIKZ01j99Y9QficUQ=="
},
"package-json": { "package-json": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
@ -3361,6 +3670,67 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true "dev": true
}, },
"socket.io": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.0.tgz",
"integrity": "sha512-/c1riZMV/4yz7KEpaMhDQbwhJDIoO55whXaRKgyEBQrLU9zUHXo9rzeTMvTOqwL9mbKfHKdrXcMoCeQ/1YtMsg==",
"requires": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.1",
"engine.io": "~5.0.0",
"socket.io-adapter": "~2.2.0",
"socket.io-parser": "~4.0.3"
},
"dependencies": {
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"socket.io-adapter": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
"integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
},
"socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"requires": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"debug": "~4.3.1"
},
"dependencies": {
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"statuses": { "statuses": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
@ -3605,6 +3975,12 @@
"typedarray-to-buffer": "^3.1.5" "typedarray-to-buffer": "^3.1.5"
} }
}, },
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"requires": {}
},
"xdg-basedir": { "xdg-basedir": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",

View File

@ -20,7 +20,9 @@
"moment": "^2.29.1", "moment": "^2.29.1",
"mustache": "^4.1.0", "mustache": "^4.1.0",
"node-forge": "^0.10.0", "node-forge": "^0.10.0",
"redis": "^2.8.0" "p2psub": "^0.1.9",
"redis": "^2.8.0",
"socket.io": "^4.0.0"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {

View File

@ -1,5 +1,67 @@
var app = {}; var app = {};
app.pubsub = (function(){
app.topics = {};
app.subscribe = function(topic, listener) {
if(topic instanceof RegExp){
listener.match = topic;
topic = "__REGEX__";
}
// create the topic if not yet created
if(!app.topics[topic]) app.topics[topic] = [];
// add the listener
app.topics[topic].push(listener);
}
app.matchTopics = function(topic){
topic = topic || '';
var matches = [... app.topics[topic] ? app.topics[topic] : []];
if(!app.topics['__REGEX__']) return matches;
for(var listener of app.topics['__REGEX__']){
if(topic.match(listener.match)) matches.push(listener);
}
return matches;
}
app.publish = function(topic, data) {
// send the event to all listeners
app.matchTopics(topic).forEach(function(listener) {
setTimeout(function(data, topic){
listener(data || {}, topic);
}, 0, data, topic);
});
}
return this;
})(app);
app.io = (function(app){
var socket = io();
// socket.emit('chat message', $('#m').val());
socket.on('P2PSub', function(msg){
msg.data.__noSocket = true;
app.publish(msg.topic, msg.data);
});
app.subscribe(/./g, function(data, topic){
// console.log('local_pubs', data, topic)
if(data.__noSocket) return;
// console.log('local_pubs 2', data, topic)
socket.emit('P2PSub', { topic, data })
});
return io;
})(app);
app.api = (function(app){ app.api = (function(app){
var baseURL = '/api/' var baseURL = '/api/'
@ -108,6 +170,7 @@ app.auth = (function(app) {
function logIn(args, callack){ function logIn(args, callack){
app.api.post('auth/login', args, function(error, data){ app.api.post('auth/login', args, function(error, data){
console.log('auth', data)
if(data.login){ if(data.login){
setToken(data.token); setToken(data.token);
} }

View File

@ -5,10 +5,11 @@ const {User} = require('../models/user');
const {Auth, AuthToken} = require('../models/auth'); const {Auth, AuthToken} = require('../models/auth');
router.post('/login', async function(req, res, next){ router.post('/login', async function(req, res, next){
try{ try{
let auth = await Auth.login(req.body); let auth = await Auth.login(req.body);
console.log('auth route', auth)
return res.json({ return res.json({
login: true, login: true,
token: auth.token.token, token: auth.token.token,

View File

@ -5,6 +5,9 @@ const {doDeploy} = require('../lib/deploy');
router.all('/', async function(req, res, next) { router.all('/', async function(req, res, next) {
try{ try{
console.log(req.body)
var event = req.headers['x-github-event']; var event = req.headers['x-github-event'];
var call = (req.body.created && 'create') || var call = (req.body.created && 'create') ||
(req.body.deleted && 'delete') || (req.body.deleted && 'delete') ||
@ -15,7 +18,7 @@ router.all('/', async function(req, res, next) {
var commit = req.body.after; var commit = req.body.after;
let repo = req.body.repository.full_name; let repo = req.body.repository.full_name;
let id = await doDeploy('create', repo, branch, sshURL, commit); // let id = await doDeploy('create', repo, branch, sshURL, commit);
res.json({id}); res.json({id});

75
nodejs/routes/repo.js Normal file
View File

@ -0,0 +1,75 @@
'use strict';
const router = require('express').Router();
const {Repo} = require('../models/repo');
const Model = Repo;
router.get('/', async function(req, res, next){
try{
return res.json({
hosts: await Model[req.query.detail ? "listDetail" : "list"]()
});
}catch(error){
return next(error);
}
});
router.post('/', async function(req, res, next){
try{
req.body.created_by = req.user.username;
await Model.add(req.body);
return res.json({
message: `"${req.body.host}" added.`
});
} catch (error){
return next(error);
}
});
router.get('/:item(*)', async function(req, res, next){
try{
return res.json({
item: req.params.item,
results: await Model.get(req.params.item)
});
}catch(error){
return next(error);
}
});
router.put('/:item(*)', async function(req, res, next){
try{
req.body.updated_by = req.user.username;
let item = await Model.get(req.params.item);
await item.update.call(item, req.body);
return res.json({
message: `"${req.params.item}" updated.`
});
}catch(error){
return next(error);
}
});
router.delete('/:item(*)', async function(req, res, next){
try{
let item = await Model.get(req.params);
let count = await host.remove.call(item, item);
return res.json({
message: `${req.params.host} deleted`,
});
}catch(error){
return next(error);
}
});
module.exports = router;

View File

@ -1,63 +0,0 @@
'use strict';
const router = require('express').Router();
const {AuthToken} = require('../models/auth');
const {Token, InviteToken} = require('../models/token');
const tokens = {
auth: AuthToken,
invite: InviteToken
}
router.get('/:name', async function(req, res, next){
try{
console.log(tokens, req.params.name)
return res.json({
results: await tokens[req.params.name][req.query.detail ? "listDetail" : "list"]()
});
}catch(error){
next(error);
}
});
router.get('/:name/:token', async function(req, res, next){
try{
return res.json({
results: await tokens[req.params.name].get(req.params.token)
});
}catch(error){
next(error);
}
});
// router.delete('/:username', async function(req, res, next){
// try{
// let user = await User.get(req.params.username);
// return res.json({username: req.params.username, results: await user.remove()})
// }catch(error){
// next(error);
// }
// });
module.exports = router;
/*
verify public ssh key
*/
// router.post('/verifykey', async function(req, res){
// let key = req.body.key;
// try{
// return res.json({
// info: await Users.verifyKey(key)
// });
// }catch(error){
// return res.status(400).json({
// message: 'Key is not a public key file!'
// });
// }
// });

View File

@ -5,63 +5,34 @@ const {User} = require('../models/user');
const {Auth, AuthToken} = require('../models/auth'); const {Auth, AuthToken} = require('../models/auth');
router.post('/login', async function(req, res, next){ router.get('/', async function(req, res, next){
try{ try{
let auth = await Auth.login(req.body);
return res.json({ return res.json({
login: true, results: await User[req.query.detail ? "listDetail" : "list"]()
token: auth.token.token,
}); });
}catch(error){ }catch(error){
next(error); next(error);
} }
}); });
router.all('/logout', async function(req, res, next){ router.get('/me', async function(req, res, next){
try{ try{
if(req.user){
await req.user.logout();
}
res.json({message: 'Bye'}) return res.json(await User.get({uid: req.user.uid}));
}catch(error){ }catch(error){
next(error); next(error);
} }
}); });
router.post('/invite/:token', async function(req, res, next) {
router.get('/:uid', async function(req, res, next){
try{ try{
req.body.token = req.params.token;
let user = await User.addByInvite(req.body);
let token = await AuthToken.add(user);
return res.json({ return res.json({
user: user.username, results: await User.get(req.params.uid),
token: token.token
}); });
}catch(error){ }catch(error){
next(error); next(error);
} }
}); });
module.exports = router; module.exports = router;
/*
verify public ssh key
*/
// router.post('/verifykey', async function(req, res){
// let key = req.body.key;
// try{
// return res.json({
// info: await Users.verifyKey(key)
// });
// }catch(error){
// return res.status(400).json({
// message: 'Key is not a public key file!'
// });
// }
// });

View File

@ -14,6 +14,10 @@ class Table{
static async get(index){ static async get(index){
try{ try{
if(typeof index === 'object'){
index = index[this._key]
}
let result = await client.HGETALL(`${this.prototype.constructor.name}_${index}`); let result = await client.HGETALL(`${this.prototype.constructor.name}_${index}`);
if(!result){ if(!result){

View File

@ -8,6 +8,7 @@
<link rel="stylesheet" href="/static/css/bootstrap-4.4.1.min.css"> <link rel="stylesheet" href="/static/css/bootstrap-4.4.1.min.css">
<link rel='stylesheet' href='/static/css/styles.css' /> <link rel='stylesheet' href='/static/css/styles.css' />
<!-- Scripts are placed here --> <!-- Scripts are placed here -->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src='/static/js/jquery-3.5.0.min.js'></script> <script type="text/javascript" src='/static/js/jquery-3.5.0.min.js'></script>
<script type="text/javascript" src="/static/js/popper-1.16.0.min.js"></script> <script type="text/javascript" src="/static/js/popper-1.16.0.min.js"></script>
<script type="text/javascript" src="/static/js/bootstrap-4.4.1.min.js"></script> <script type="text/javascript" src="/static/js/bootstrap-4.4.1.min.js"></script>