simple proxmox

This commit is contained in:
William Mantly 2020-08-26 15:41:37 -04:00
parent f7cee0239e
commit f18967ce8b
Signed by: wmantly
GPG Key ID: 186A8370EFF937CA
26 changed files with 964 additions and 2504 deletions

View File

@ -3,16 +3,42 @@
const path = require('path'); const path = require('path');
const ejs = require('ejs') const ejs = require('ejs')
const express = require('express'); const express = require('express');
const {P2PSub} = require('p2psub');
const pvejs = require('./utils/pvejs');
// Set up the express app. // Set up the express app.
const app = express(); const app = express();
app.onListen = [];
// 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');
app.p2p = new P2PSub(app.conf.p2p);
app.onListen.push(function(){
app.p2p.subscribe(/./g, function(data, topic){
if(data.__noSocket) return;
app.io.emit('P2PSub', { topic, data })
});
app.io.on('connection', (socket) => {
// console.log('connection io', socket)
socket.on('P2PSub', (msg) => {
msg.data.__noSocket = true;
app.p2p.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');
@ -39,8 +65,29 @@ app.use('/api/user', middleware.auth, require('./routes/user'));
app.use('/api/token', middleware.auth, require('./routes/token')); app.use('/api/token', middleware.auth, require('./routes/token'));
app.use('/api/group', middleware.auth, require('./routes/group')); (async function(){
app.api = await pvejs(app.conf.proxmox);
})()
setInterval(async function(){
try{
let res = await app.api.GET({path: '/cluster/resources'});
let types = {};
for(let item of res.json){
if(!types[item.type]){
types[item.type] = [];
}
types[item.type].push(item);
}
app.p2p.publish('proxmox-cluster', {vpnSite: app.conf.vpnSite, data: types});
}catch(error){
console.error('proxmox pub', error)
}
}, 10000);
// Catch 404 and forward to error handler. If none of the above routes are // Catch 404 and forward to error handler. If none of the above routes are
// used, this is what will be called. // used, this is what will be called.

View File

@ -21,6 +21,10 @@ 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,4 +91,8 @@ function onListening() {
? 'pipe ' + addr ? 'pipe ' + addr
: 'port ' + addr.port; : 'port ' + addr.port;
debug('Listening on ' + bind); debug('Listening on ' + bind);
for(let listener of app.onListen){
listener()
}
} }

View File

@ -3,13 +3,27 @@
module.exports = { module.exports = {
userModel: 'ldap', // pam, redis, ldap userModel: 'ldap', // pam, redis, ldap
ldap: { ldap: {
url: 'ldap://192.168.1.54:389', url: 'ldap://192.168.1.55:389',
bindDN: 'cn=admin,dc=theta42,dc=com', bindDN: 'cn=ldapclient service,ou=people,dc=theta42,dc=com',
bindPassword: '__IN SRECREST FILE__', bindPassword: '__IN SRECREST FILE__',
userBase: 'ou=people,dc=theta42,dc=com', userBase: 'ou=people,dc=theta42,dc=com',
groupBase: 'ou=groups,dc=theta42,dc=com', groupBase: 'ou=groups,dc=theta42,dc=com',
userFilter: '(objectClass=posixAccount)', userFilter: '(objectClass=posixAccount)',
userNameAttribute: 'uid' userNameAttribute: 'uid'
}, },
SENDGRID_API_KEy: '__IN SRECREST FILE__', p2p: {
listenPort: 7575
},
proxmox: {
host: "__IN SRECREST FILE__",//this can be an ip or FQDN
authInfo: {
username: "__IN SRECREST FILE__",//this must include the username@realm
apiToken: "__IN SRECREST FILE__"//In the future, i would like this to be encrypted
},
},
vpnSite: {
id: "__IN SRECREST FILE__",
name: "__IN SRECREST FILE__",
admin: "__IN SRECREST FILE__"
}
}; };

View File

@ -1,32 +0,0 @@
'use strict';
const sgMail = require('@sendgrid/mail');
const mustache = require('mustache');
const conf = require('../app').conf;
sgMail.setApiKey(conf.SENDGRID_API_KEY);
var Mail = {};
Mail.send = async function(to, subject, message, from){
await sgMail.send({
to: to,
from: from || 'Theta 42 Accounts <accounts@no-reply.theta42.com>',
subject: subject,
text: message,
html: message,
});
};
Mail.sendTemplate = async function(to, template, context, from){
template = require(`../views/email_templates/${template}`);
await Mail.send(
to,
mustache.render(template.subject, context),
mustache.render(template.message, context),
from || (template.from && mustache.render(template.message, context))
)
};
module.exports = {Mail};

View File

@ -27,61 +27,6 @@ async function getGroups(client, member){
} }
} }
async function addGroup(client, data){
try{
await client.add(`cn=${data.name},${conf.groupBase}`, {
cn: data.name,
member: data.owner,
description: data.description,
owner: data.owner,
objectclass: [ 'groupOfNames', 'top' ]
});
return data;
}catch(error){
throw error;
}
}
async function addMember(client, group, user){
try{
await client.modify(group.dn, [
new Change({
operation: 'add',
modification: new Attribute({
type: 'member',
values: [user.dn]
})
}),
]);
}catch(error){
// if(error = "TypeOrValueExistsError"){
// console.error('addMember error skipped', error)
// return ;
// }
throw error;
}
}
async function removeMember(client, group, user){
try{
await client.modify(group.dn, [
new Change({
operation: 'delete',
modification: new Attribute({
type: 'member',
values: [user.dn]
})}),
]);
}catch(error){
if(error = "TypeOrValueExistsError")return ;
throw error;
}
}
var Group = {}; var Group = {};
Group.list = async function(member){ Group.list = async function(member){
@ -151,63 +96,4 @@ Group.get = async function(data){
} }
} }
Group.add = async function(data){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await addGroup(client, data);
await client.unbind();
return this.get(data);
}catch(error){
throw error;
}
}
Group.addMember = async function(user){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await addMember(client, this, user);
await client.unbind();
return this;
}catch(error){
throw error;
}
};
Group.removeMember = async function(user){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await removeMember(client, this, user);
await client.unbind();
return this;
}catch(error){
throw error;
}
};
Group.remove = async function(){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await client.del(this.dn);
await client.unbind();
return true;
}catch(error){
throw error;
}
}
module.exports = {Group}; module.exports = {Group};

View File

@ -3,7 +3,6 @@
const { Client, Attribute, Change } = require('ldapts'); const { Client, Attribute, Change } = require('ldapts');
const crypto = require('crypto'); const crypto = require('crypto');
const {Mail} = require('./email');
const {Token, InviteToken, PasswordResetToken} = require('./token'); const {Token, InviteToken, PasswordResetToken} = require('./token');
const conf = require('../app').conf.ldap; const conf = require('../app').conf.ldap;
@ -11,106 +10,6 @@ const client = new Client({
url: conf.url, url: conf.url,
}); });
async function addPosixGroup(client, data){
try{
const groups = (await client.search(conf.groupBase, {
scope: 'sub',
filter: '(&(objectClass=posixGroup))',
})).searchEntries;
data.gidNumber = (Math.max(...groups.map(i => i.gidNumber))+1)+'';
await client.add(`cn=${data.cn},${conf.groupBase}`, {
cn: data.cn,
gidNumber: data.gidNumber,
objectclass: [ 'posixGroup', 'top' ]
});
return data;
}catch(error){
throw error;
}
}
async function addPosixAccount(client, data){
try{
const people = (await client.search(conf.userBase, {
scope: 'sub',
filter: conf.userFilter,
})).searchEntries;
data.uidNumber = (Math.max(...people.map(i => i.uidNumber))+1)+'';
await client.add(`cn=${data.cn},${conf.userBase}`, {
cn: data.cn,
sn: data.sn,
uid: data.uid,
uidNumber: data.uidNumber,
gidNumber: data.gidNumber,
givenName: data.givenName,
mail: data.mail,
mobile: data.mobile,
loginShell: data.loginShell,
homeDirectory: data.homeDirectory,
userPassword: data.userPassword,
description: data.description || ' ',
sudoHost: 'ALL',
sudoCommand: 'ALL',
sudoUser: data.uid,
sshPublicKey: data.sshPublicKey,
objectclass: ['inetOrgPerson', 'sudoRole', 'ldapPublicKey', 'posixAccount', 'top' ]
});
return data
}catch(error){
throw error;
}
}
async function addLdapUser(client, data){
var group;
try{
data.uid = `${data.givenName[0]}${data.sn}`;
data.cn = data.uid;
data.loginShell = '/bin/bash';
data.homeDirectory= `/home/${data.uid}`;
data.userPassword = '{MD5}'+crypto.createHash('md5').update(data.userPassword, "binary").digest('base64');
group = await addPosixGroup(client, data);
data = await addPosixAccount(client, group);
return data;
}catch(error){
await deleteLdapDN(client, `cn=${data.uid},${conf.groupBase}`, true);
throw error;
}
}
async function deleteLdapUser(client, data){
try{
await client.del(`cn=${data.cn},${conf.groupBase}`);
await client.del(data.dn);
}catch(error){
throw error;
}
}
async function deleteLdapDN(client, dn, ignoreError){
try{
client.del(dn)
}catch(error){
if(!ignoreError) throw error;
console.error('ERROR: deleteLdapDN', error)
}
}
const user_parse = function(data){ const user_parse = function(data){
if(data[conf.userNameAttribute]){ if(data[conf.userNameAttribute]){
data.username = data[conf.userNameAttribute] data.username = data[conf.userNameAttribute]
@ -225,195 +124,6 @@ User.exists = async function(data, key){
} }
}; };
User.add = async function(data) {
try{
await client.bind(conf.bindDN, conf.bindPassword);
await addLdapUser(client, data);
await client.unbind();
let user = await this.get(data.uid);
await Mail.sendTemplate(
user.mail,
'welcome',
{
user: user
}
)
return user;
}catch(error){
if(error.message.includes('exists')){
let error = new Error('UserNameUsed');
error.name = 'UserNameUsed';
error.message = `LDAP:${data.uid} already exists`;
error.status = 409;
throw error;
}
throw error;
}
};
User.update = async function(data){
try{
let editableFeilds = ['mobile', 'sshPublicKey', 'description'];
await client.bind(conf.bindDN, conf.bindPassword);
for(let field of editableFeilds){
if(data[field]){
await client.modify(this.dn, [
new Change({
operation: 'replace',
modification: new Attribute({
type: field,
values: [data[field]]
})
}),
]);
}
}
await client.unbind()
return this;
}catch(error){
throw error;
}
};
User.addByInvite = async function(data){
try{
let token = await InviteToken.get(data.token);
if(!token.is_valid && data.mailToken !== token.mail_token){
let error = new Error('Token Invalid');
error.name = 'Token Invalid';
error.message = `Token is not valid or as allready been used. ${data.token}`;
error.status = 401;
throw error;
}
data.mail = token.mail;
let user = await this.add(data);
if(user){
await token.consume({claimed_by: user.uid});
return user;
}
}catch(error){
throw error;
}
};
User.verifyEmail = async function(data){
try{
let exists = await this.exists(data.mail, 'mail');
if(exists) throw new Error('EmailInUse');
let token = await InviteToken.get(data.token);
await token.update({mail: data.mail})
await Mail.sendTemplate(
data.mail,
'validate_link',
{
link:`${data.url}/login/invite/${token.token}/${token.mail_token}`
}
)
return this;
}catch(error){
throw error;
}
};
User.passwordReset = async function(url, mail){
try{
let user = await User.get({
searchKey: 'mail',
searchValue: mail
});
let token = await PasswordResetToken.add(user);
await Mail.sendTemplate(
user.mail,
'reset_link',
{
user: user,
link:`${url}/login/resetpassword/${token.token}`
}
)
return true;
}catch(error){
// if(error.name === 'UserNotFound') return false;
throw error;
}
};
User.remove = async function(data){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await deleteLdapUser(client, this);
await client.unbind();
return true;
}catch(error){
throw error;
}
};
User.setPassword = async function(data){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await client.modify(this.dn, [
new Change({
operation: 'replace',
modification: new Attribute({
type: 'userPassword',
values: ['{MD5}'+crypto.createHash('md5').update(data.userPassword, "binary").digest('base64')]
})}),
]);
await client.unbind();
return this;
}catch(error){
throw error;
}
};
User.invite = async function(){
try{
let token = await InviteToken.add({created_by: this.uid});
return token;
}catch(error){
throw error;
}
};
User.login = async function(data){ User.login = async function(data){
try{ try{
let user = await this.get(data.uid); let user = await this.get(data.uid);

1214
nodejs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,16 +12,16 @@
"start": "node ./bin/www" "start": "node ./bin/www"
}, },
"dependencies": { "dependencies": {
"@sendgrid/mail": "^7.1.0",
"ejs": "^3.0.1", "ejs": "^3.0.1",
"express": "~4.16.1", "express": "~4.16.1",
"extend": "^3.0.2", "extend": "^3.0.2",
"ldapts": "^2.2.1", "ldapts": "^2.2.1",
"moment": "^2.25.3", "moment": "^2.25.3",
"mustache": "^4.0.1", "mustache": "^4.0.1",
"nodemon": "^2.0.4", "node-fetch": "^2.6.0",
"p2psub": "^0.1.6",
"redis": "^2.8.0", "redis": "^2.8.0",
"smtpc": "^0.1.2" "socket.io": "^2.3.0"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {

View File

@ -1,5 +1,68 @@
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/'

View File

@ -1,9 +1,7 @@
'use strict'; 'use strict';
const router = require('express').Router(); const router = require('express').Router();
const {User} = require('../models/user');
const {Auth, AuthToken} = require('../models/auth'); const {Auth, AuthToken} = require('../models/auth');
const {PasswordResetToken} = require('../models/token');
router.post('/login', async function(req, res, next){ router.post('/login', async function(req, res, next){
@ -31,86 +29,4 @@ router.all('/logout', async function(req, res, next){
} }
}); });
router.post('/resetpassword', async function(req, res, next){
try{
let sent = await User.passwordReset(`${req.protocol}://${req.hostname}`, req.body.mail);
console.info('resetpassword for', req.body.mail, 'sent')
return res.json({
message: 'If the emaill address is in our system, you will receive a message.'
});
}catch(error){
next(error);
}
});
router.post('/resetpassword/:token', async function(req, res, next){
try{
let token = await PasswordResetToken.get(req.params.token);
if(token.is_valid && 86400000+Number(token.created_on) > (new Date).getTime()){
let user = await User.get(token.created_by);
await user.setPassword(req.body);
token.update({is_valid: false});
return res.json({
message: 'Password has been changed.'
});
}
}catch(error){
next(error);
}
});
router.post('/invite/:token/:mailToken', async function(req, res, next) {
try{
req.body.token = req.params.token;
req.body.mailToken = req.params.mailToken;
let user = await User.addByInvite(req.body);
let token = await AuthToken.add(user);
return res.json({
user: user.uid,
token: token.token
});
}catch(error){
next(error);
}
});
router.post('/invite/:token', async function(req, res, next){
try{
let data = {
token: req.params.token,
url: `${req.protocol}://${req.hostname}`,
mail: req.body.mail,
}
await User.verifyEmail(data);
return res.send({message: 'sent'});
}catch(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

@ -1,82 +0,0 @@
'use strict';
const router = require('express').Router();
const {User} = require('../models/user_ldap');
const {Group} = require('../models/group_ldap');
router.get('/', async function(req, res, next){
try{
let member = req.query.member ? await User.get(req.query.member) : {}
console.log('member', member)
return res.json({
results: await Group[req.query.detail ? "listDetail" : "list"](member.dn)
});
}catch(error){
next(error);
}
});
router.post('/', async function(req, res, next){
try{
req.body.owner = req.user.dn;
return res.json({
results: await Group.add(req.body),
message: `${req.body.name} was added!`
})
}catch(error){
next(error);
}
});
router.get('/:name', async function(req, res, next){
try{
return res.json({
results: await Group.get(req.params.name)
});
}catch(error){
next(error);
}
});
router.put('/:name/:uid', async function(req, res, next){
try{
var group = await Group.get(req.params.name);
var user = await User.get(req.params.uid);
return res.json({
results: group.addMember(user),
message: `Added user ${req.params.uid} to ${req.params.name} group.`
});
}catch(error){
next(error);
}
});
router.delete('/:name/:uid', async function(req, res, next){
try{
var group = await Group.get(req.params.name);
var user = await User.get(req.params.uid);
return res.json({
results: group.removeMember(user),
message: `Removed user ${req.params.uid} from ${req.params.name} group.`
});
}catch(error){
next(error);
}
});
router.delete('/:name', async function(req, res, next){
try{
var group = await Group.get(req.params.name);
return res.json({
removed: await group.remove(),
results: group,
message: `Group ${req.params.name} Deleted`
});
}catch(error){
next(error);
}
});
module.exports = router;

View File

@ -12,64 +12,21 @@ router.get('/', async function(req, res, next) {
}); });
/* GET home page. */ /* GET home page. */
router.get('/users', function(req, res, next) {
res.render('users', { title: 'Express' }); router.get('/topics', function(req, res, next) {
res.render('topics', { title: 'Express' });
}); });
router.get('/users/:uid', function(req, res, next) { router.get('/chat', function(req, res, next) {
res.render('home', { title: 'Express' }); res.render('chat', { title: 'Express' });
}); });
router.get('/groups', function(req, res, next) {
res.render('groups', { title: 'Express' });
});
router.get('/login/resetpassword/:token', async function(req, res, next){
let token = await PasswordResetToken.get(req.params.token);
if(token.is_valid && 86400000+Number(token.created_on) > (new Date).getTime()){
res.render('reset_password', {token:token});
}else{
next({message: 'token not found', status: 404});
}
});
router.get('/login/invite/:token/:mailToken', async function(req, res, next){
try{
let token = await InviteToken.get(req.params.token);
if(token.is_valid && token.mail !== '__NONE__' && token.mail_token === req.params.mailToken){
token.created_on = moment(token.created_on, 'x').fromNow();
res.render('invite', { title: 'Express', invite: token });
}else{
next({message: 'token not found', status: 404});
}
}catch(error){
next(error);
}
});
router.get('/login/invite/:token', async function(req, res, next){
try{
let token = await InviteToken.get(req.params.token);
token.created_on = moment(token.created_on, 'x').fromNow();
if(token.is_valid){
res.render('invite_email', { title: 'Express', invite: token });
}else{
next({message: 'token not found', status: 404});
}
}catch(error){
next(error);
}
});
/* GET home page. */
router.get('/login', function(req, res, next) { router.get('/login', function(req, res, next) {
res.render('login', {redirect: req.query.redirect}); res.render('login', {redirect: req.query.redirect});
}); });
router.get('/proxmox', function(req, res, next) {
res.render('proxmox', {});
});
module.exports = router; module.exports = router;

83
nodejs/routes/user.js Executable file → Normal file
View File

@ -13,42 +13,6 @@ router.get('/', async function(req, res, next){
} }
}); });
router.post('/', async function(req, res, next){
try{
req.body.created_by = req.user.uid
return res.json({results: await User.add(req.body)});
}catch(error){
next(error);
}
});
router.delete('/:uid', async function(req, res, next){
try{
let user = await User.get(req.params.uid);
return res.json({uid: req.params.uid, results: await user.remove()})
}catch(error){
next(error);
}
});
router.put('/:uid', async function(req, res, next){
try{
let user = await User.get(req.params.uid);
// console.log('update user', user);
return res.json({
results: await user.update(req.body),
message: `Updated ${req.params.uid} user`
});
}catch(error){
next(error);
}
});
router.get('/me', async function(req, res, next){ router.get('/me', async function(req, res, next){
try{ try{
@ -58,53 +22,6 @@ router.get('/me', async function(req, res, next){
} }
}); });
router.put('/password', async function(req, res, next){
try{
return res.json({results: await req.user.setPassword(req.body)})
}catch(error){
next(error);
}
});
router.put('/:uid/password', async function(req, res, next){
try{
let user = await User.get(req.params.uid);
return res.json({
results: await user.setPassword(req.body),
message: `User ${user.uid} password changed.`
});
}catch(error){
next(error);
}
});
router.post('/invite', async function(req, res, next){
try{
let token = await req.user.invite();
return res.json({token: token.token});
}catch(error){
next(error);
}
});
router.post('/key', async function(req, res, next){
try{
let added = await User.addSSHkey({
uid: req.user.uid,
key: req.body.key
});
return res.status(added === true ? 200 : 400).json({
message: added
});
}catch(error){
next(error);
}
});
router.get('/:uid', async function(req, res, next){ router.get('/:uid', async function(req, res, next){
try{ try{
return res.json({ return res.json({

214
nodejs/utils/pvejs.js Normal file
View File

@ -0,0 +1,214 @@
'use strict';
const fetch = require('node-fetch');
const { URLSearchParams } = require('url');
var apiProto = {};
apiProto.__getApiDoc = async function(host){
try{
let setupRes = await fetch(host || this.host+'/pve-docs/api-viewer/apidoc.js', { method: 'GET'});
let text = await setupRes.text()
text = text.split('// avoid errors when running without development tools')[0];
return eval(text += '; pveapi;');
}catch(error){
throw error;
}
}
apiProto.__makeMap = async function(APItree){
APItree = APItree || await this.__getApiDoc();
let apiMap = {};
function __rec(obj){
for(let item of obj){
if(item.children){
__rec(item.children);
}
if(item.path){
delete item.children;
apiMap[item.path] = item;
item.methods = item.info;
delete item.info;
}
}
}
__rec(APItree);
return apiMap
};
//these helper functions are to build the fetch call
apiProto.__buildHeaders = function(){
let headers = {};
if(this.authInfo.apiToken){
headers = {
"Authorization": `PVEAPIToken=${this.authInfo.username}=${this.authInfo.apiToken}`
}
}else{
headers = {
'Cookie': 'PVEAuthCookie='+this.user.ticket,
'CSRFPreventionToken': this.user.CSRFPreventionToken
}
}
return headers;
};
// this function takes an iterable of 'key':'value' pairs
apiProto.__buildBody = function(props, data){
const params = new URLSearchParams();
for (let key in props){
if(data[key]){
params.append(key, data[key])
continue;
}
if(props[key].optional !==1){
// console.log(props[key])
throw new Error('MissingKey');
}
}
return params;
};
apiProto.__getEndPoint = function(args){
let endpoint = this.apiMap[args.path]
if(!args || !endpoint) throw new Error('endpointNotFound');
if(!endpoint.methods[args.method]) throw new Error('methodNotFound');
return endpoint;
};
apiProto.__fetch = async function(method, path, params, data){
try{
let endpoint = this.__getEndPoint({path, method});
let fetchOptions = {
method: method,
headers: this.__buildHeaders(),
}
if(['PUT', 'POST'].includes(method)){
fetchOptions.body = this.__buildBody(
endpoint.methods[method].parameters.properties,
data
)
}
let HTTPres = await fetch(this.BASEURL+path, fetchOptions);
// console.log(HTTPres)
return {
statusCode: HTTPres.status,
statusText: HTTPres.statusText,
json: (await HTTPres.json()).data,
}
}catch(error){
throw error;
}
};
apiProto.GET = async function(args){
try{
return await this.__fetch(
'GET',
args.path,
args.parama || {},
);
}catch(error){
throw error;
}
};
apiProto.DELETE = async function(args){
try{
return await this.__fetch(
'DELETE',
args.path,
args.parama || {},
);
}catch(error){
throw error;
}
};
apiProto.POST = async function(args){
try{
return await this.__fetch(
'POST',
args.path,
args.parama || {},
args.data || {}
);
}catch(error){
throw error;
}
};
apiProto.PUT = async function(args){
try{
return await this.__fetch(
'PUT',
args.path,
args.parama || {},
args.data || {}
);
}catch(error){
throw error;
}
};
apiProto.auth = async function(authInfo){
try{
this.user = (await this.POST({
path: '/access/ticket',
data: authInfo || this.authInfo
})).json
return authInfo;
}catch(error){
throw error;
}
};
apiProto.__init = async function(conf){
try{
this.user = {};
this.BASEURL = conf.host+'/api2/json';
this.apiMap = await this.__makeMap();
if(!this.authInfo.apiToken) await this.auth();
return this;
}catch(error){
throw(error);
}
};
async function API(conf){
let instance = Object.create(apiProto);
Object.assign(instance, conf);
return await instance.__init(conf);
}
module.exports = API;

79
nodejs/views/chat.ejs Normal file
View File

@ -0,0 +1,79 @@
<%- include('top') %>
<script id="rowTemplate" type="text/html">
<p>
<b>{{ uid }}:</b> {{message}}
</p>
</script>
<script type="text/javascript">
var publishTopic = function(btn){
event.preventDefault(); // avoid to execute the actual submit of the form.
var $form = $(btn).closest( '[action]' ); // gets the 'form' parent
var formData = $form.find( '[name]' ).serializeObject();
if( !$form.validate()) {
app.util.actionMessage('Please fix the form errors.', $form, 'danger')
return false;
}
console.log('formData', formData)
app.publish('p2p-chat', {
uid: app.auth.user.uid,
message: formData.message
})
$form.trigger("reset");
};
$(document).ready(function(){
app.subscribe("p2p-chat", function(data, topic){
var rowTemplate = $('#rowTemplate').html();
var $target = $('#tableAJAX');
user_row = Mustache.render(rowTemplate, data);
$target.append(user_row);
});
});
</script>
<div class="row" style="display:none">
<div class="col-md-4">
<div class="card shadow-lg">
<div class="card-header">
<i class="fas fa-layer-plus"></i>
Chat
</div>
<div class="card-header actionMessage" style="display:none"></div>
<div class="card-body">
<form action='__internal__' onsubmit="publishTopic(this)">
<div class="form-group">
<label class="control-label">message</label>
<textarea class="form-control shadow" name="message" placeholder="{...}" validate=":1"></textarea>
</div>
<button type="submit" class="btn btn-outline-dark">Send</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow-lg">
<div class="card-header">
<i class="fad fa-users-class"></i>
Incoming Chats
</div>
<div class="card-header actionMessage" style="display:none">
</div>
<div class="card-body">
<div class="" id="tableAJAX">
</div>
</div>
</div>
</div>
</div>
<%- include('bottom') %>

View File

@ -1,25 +0,0 @@
module.exports = {
subject: 'Password reset for Theta 42 account',
message: `
<h2> Theta 42 account</h2>
<p>
Hello {{ user.givenName }},
</p>
<p>
You have asked to reset the password for user name <b>{{ user.uid }}</b> . Please
click the link below to complete this request. If this was done in errror,
please ignore this email.
</p>
<p>
{{ link }}
</p>
</p>
Thank you,<br />
Theta 42
</p>
`
};

View File

@ -1,24 +0,0 @@
module.exports = {
subject: 'Validate email for Theta 42 account',
message: `
<h2> Theta 42 account</h2>
<p>
Welcome,
</p>
<p>
We need to verify the provided email address in order to continue. Please
follow the link below to verify this email address:
</p>
<p>
{{ link }}
</p>
</p>
Thank you,<br />
Theta 42
</p>
`
};

View File

@ -1,34 +0,0 @@
module.exports = {
subject: 'Welcome to Theta 42!',
message: `
<p>
Welcome {{user.givenName}},
</p>
<p>
Your new Theta 42 Single sign-on account is ready to use. Here is some
information to get you started.
</p>
<p>
Your username is <b>{{user.uid}}</b>
</p>
<p>
You can manage your account at https://sso.theta42.com
</p>
<p>
You account is ready to be used now, test it by SSHing into the Theta 42
jump host \`ssh {{user.uid}}@718it.biz\`
</p>
<p>
The SSO service is still in beta, so please report any bugs you may find!
You will be notified of new features and services as they become available.
</p>
Thank you,<br />
Theta 42
</p>
`
};

View File

@ -1,134 +0,0 @@
<%- include('top') %>
<script id="rowTemplate" type="text/html">
<p>
<div class="card shadow">
<div class="card-header">
<i class="fad fa-users-class"></i>
Group: {{ cn }}
</div>
<div class="card-body">
<p>
{{ description }}
</p>
<p>
<ul class="list-group">
{{ #member }}
<li class="list-group-item shadow">
<i class="fad fa-user"></i> {{ uid }}
<button type="button" action="group/{{groupCN}}/{{uid}}" method="delete" onclick="formAJAX(this)" evalAJAX="tableAJAX(data.message)" class="btn btn-sm btn-danger float-right">
<i class="fad fa-user-slash"></i>
</button>
</li>
{{ /member }}
</ul>
</p>
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fad fa-user-plus"></i>
</button>
<div class="dropdown-menu shadow-lg" aria-labelledby="dropdownMenuButton">
{{ #toAdd }}{{#.}}
<a class="dropdown-item" action="group/{{groupCN}}/{{uid}}" method="put" onclick="formAJAX(this)" evalAJAX="tableAJAX(data.message)">
<i class="fad fa-user"></i> {{uid}}
</a>
{{/.}}{{ /toAdd }}
</div>
<button type="button" onclick="app.group.remove({cn: '{{cn}}'}, function(){tableAJAX('Group {{cn}} deleted.')})" class="btn btn-danger float-right">
<i class="fad fa-trash"></i>
</button>
</div>
</div>
</p>
</script>
<script type="text/javascript">
var userlist;
function getUserList(callback){
app.user.list(function(error, data){
userlist = data.results;
callback()
})
}
function tableAJAX(actionMessage){
var rowTemplate = $('#rowTemplate').html();
var $target = $('#tableAJAX');
$target.html('').hide();
app.util.actionMessage('Refreshing user list...', $target);
app.group.list(function(error, data){
$.each( data.results, function(key, value) {
// console.log(value.member)
value.toAdd = userlist.map(function(user){
if(!value.member.includes(user.dn)) return user;
})
value.member = value.member.map(function(user){
return {
dn: user,
uid: user.match(/cn=[a-zA-Z0-9\_\-\@\.]+/)[0].replace('cn=', '')
}
})
value.groupCN = value.cn;
user_row = Mustache.render(rowTemplate, value);
$target.append(user_row);
});
$target.fadeIn('slow');
app.util.actionMessage(actionMessage || '', $target, 'info');
});
}
$(document).ready(function(){
getUserList(tableAJAX);
});
</script>
<div class="row" style="display:none">
<div class="col-md-4">
<div class="card shadow-lg">
<div class="card-header">
<i class="fas fa-layer-plus"></i>
Add new group
</div>
<div class="card-header actionMessage" style="display:none"></div>
<div class="card-body">
<form action="group/" method="post" onsubmit="formAJAX(this)" evalAJAX="tableAJAX('')">
<div class="form-group">
<label class="control-label">Name</label>
<input type="text" class="form-control shadow" name="name" placeholder="app_gitea_admin" validate=":3" />
</div>
<div class="form-group">
<label class="control-label">Description</label>
<textarea class="form-control shadow" name="description" placeholder="Admin group for gitea app" validate=":3"></textarea>
</div>
<button type="submit" class="btn btn-outline-dark">Add</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow-lg">
<div class="card-header">
<i class="fad fa-users-class"></i>
Group list
</div>
<div class="card-header actionMessage" style="display:none">
</div>
<div class="card-body">
<div class="" id="tableAJAX">
</div>
</div>
</div>
</div>
</div>
<%- include('bottom') %>

View File

@ -1,39 +0,0 @@
<%- include('top') %>
<script type="text/javascript">
function tableAJAX(message){
app.util.actionMessage(message);
}
$(document).ready(function(){
$('form').attr('action', 'auth/invite/<%= invite.token %>/<%= invite.mail_token %>').attr('evalAJAX', 'location.replace("/login");')
$('[name="mail"').val('<%= invite.mail %>').prop("disabled", true);
});
</script>
<style type="text/css">
div.form-group:hover {
-ms-transform: scale(1.02);
-webkit-transform: scale(1.02);
transform: scale(1.02);
}
</style>
<div class="row" style="display:none">
<div class="col-md-12">
<div class="card shadow-lg">
<div class="card-header">
Add new user
</div>
<div class="card-header actionMessage" style="display:none">
</div>
<div class="card-body">
<p>
Invited By: <b><%= invite.created_by %></b>, <%= invite.created_on %>
</p>
<%- include('user_form') %>
</div>
</div>
</div>
</div>
<%- include('bottom') %>

View File

@ -1,59 +0,0 @@
<%- include('top') %>
<script type="text/javascript">
var emailSent = function(){
$('#email_card .card-body').html("<h1>Thank you!</h1><p>Check your mail</p>")
}
$(document).ready(function(){
});
</script>
<style type="text/css">
div.form-group:hover {
-ms-transform: scale(1.02);
-webkit-transform: scale(1.02);
transform: scale(1.02);
}
</style>
<div class="row">
<div id="email_card" class="card-deck">
<div class="shadow-lg card mb-3">
<div class="card-header">
Validate Email
</div>
<div class="card-header actionMessage" style="display:none">
</div>
<div class="card-body">
<p>
Invited By: <b><%= invite.created_by %></b>, <%= invite.created_on %>.
</p>
<p>
Please enter a valid email address. A link will be sent to
the supplied address to complete the registration process.
</p>
<p>
The supplied email will also be used as the linked email for
the new user.
</p>
<form action="auth/invite/<%= invite.token %>" onsubmit="formAJAX(this)" evalAJAX="emailSent()">
<div class="form-group">
<label class="control-label">Email</label>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" ><i class="fad fa-at"></i></span>
</div>
<input type="email" name="mail" class="form-control" placeholder="jsmith@gmail.com" validate="email:3" />
</div>
</div>
<button type="submit" class="btn btn-outline-dark"><i class="fad fa-paper-plane"></i> Send It!</button>
</form>
</div>
</div>
</div>
</div>
</div>
<%- include('bottom') %>

View File

@ -3,7 +3,7 @@
app.auth.isLoggedIn(function(error, isLoggedIn){ app.auth.isLoggedIn(function(error, isLoggedIn){
if(isLoggedIn){ if(isLoggedIn){
window.location.href = app.util.getUrlParameter('redirect') || '/'; window.location.href = app.util.getUrlParameter('redirect') || '/topics';
} }
}) })
@ -48,53 +48,6 @@
</form> </form>
</div> </div>
</div> </div>
<div class="shadow-lg card border-danger mb-3">
<div class="card-header shadow">
Social Login
</div>
<div class="card-header shadow actionMessage" style="display:none">
</div>
<div class="card-body">
<h3>Coming soon!</h3>
<p>
<ul class="list-group">
<li class="list-group-item"><i class="fab fa-google"></i> Login with google OATH</li>
<li class="list-group-item"><i class="fab fa-github"></i> Login with github OATH</li>
<li class="list-group-item"><i class="fab fa-facebook"></i> Login with facebook OATH</li>
</ul>
</p>
</div>
</div>
<div class="shadow-lg card mb-3">
<div class="card-header shadow">
Password Reset
</div>
<div class="card-header shadow actionMessage" style="display:none">
</div>
<div class="card-body">
<p>
Forgot your password? Or your user name? No problem! Just
enter you email address below and if you are in our system,
we will email with the required information to get back up
and running!
</p>
<form action="auth/resetpassword" onsubmit="formAJAX(this)">
<input type="hidden" name="redirect" value="<%= redirect %>">
<div class="form-group">
<label class="control-label">Email</label>
<div class="input-group mb-3 shadow">
<div class="input-group-prepend">
<span class="input-group-text" ><i class="fad fa-at"></i></span>
</div>
<input type="email" name="mail" class="form-control" placeholder="jsmith@gmail.com" validate="email:3" />
</div>
</div>
<button type="submit" class="btn btn-outline-dark"><i class="fad fa-question"></i> Help me!</button>
</form>
</div>
</div>
</div> </div>
</div> </div>
<%- include('bottom') %> <%- include('bottom') %>

114
nodejs/views/proxmox.ejs Normal file
View File

@ -0,0 +1,114 @@
<%- include('top') %>
<script type="text/javascript">
var proxmoxSites = {};
function formatBytes(a,b=2){if(0===a)return"0 Bytes";const c=0>b?0:b,d=Math.floor(Math.log(a)/Math.log(1024));return parseFloat((a/Math.pow(1024,d)).toFixed(c))+" "+["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"][d]}
var parseNodeData = function(data){
cluster = data.data
cluster.onlineCount = 0;
cluster.cpu = 0
cluster.maxcpu = 0
cluster.maxmem = 0
cluster.mem = 0
for(let node of data.data.node){
// console.log('parse', node)
if(node.status === "offline") continue;
cluster.onlineCount++
cluster.cpu += node.cpu
cluster.maxcpu += node.maxcpu
cluster.maxmem += node.maxmem
cluster.mem += node.mem
}
cluster.cpu = parseFloat(((cluster.cpu/cluster.onlineCount)*100).toFixed(2));
cluster.maxmem = formatBytes(cluster.maxmem)
cluster.mem = formatBytes(cluster.mem)
$.extend(data.data, cluster);
return data;
}
$(document).ready(function(){
app.subscribe('proxmox-cluster', function(data){
if(!proxmoxSites[data.vpnSite.name]){
let index = $.scope.proxmox.push(parseNodeData(data));
proxmoxSites[data.vpnSite.name] = {
data:data,
index: index
}
}else{
$.scope.proxmox.update(proxmoxSites[data.vpnSite.name].index, parseNodeData(data));
}
});
});
</script>
<div class="row">
<div class="shadow-lg card mb-3" jq-repeat="proxmox">
<div class="card-header">
{{ vpnSite.name }}
</div>
<div class="card-header actionMessage" style="display:none">
</div>
<div class="card-body">
Nodes online {{data.onlineCount}} of {{data.node.length}} <br />
CPU: {{data.cpu}}% of {{data.maxcpu}} cores <br />
Ram: {{data.mem}} of {{data.maxmem}}
<ul>
{{#data.lxc}}
<li>{{name}} -- {{status}}</li>
{{/data.lxc}}
</ul>
</div>
</div>
</div>
<!-- <div id="email_card" class="card-deck">
<div class="shadow-lg card mb-3">
<div class="card-header">
Validate Email
</div>
<div class="card-header actionMessage" style="display:none">
</div>
<div class="card-body">
<p>
Invited By: <b></b>,.
</p>
<p>
Please enter a valid email address. A link will be sent to
the supplied address to complete the registration process.
</p>
<p>
The supplied email will also be used as the linked email for
the new user.
</p>
<form action="auth/invite/" onsubmit="formAJAX(this)" evalAJAX="emailSent()">
<div class="form-group">
<label class="control-label">Email</label>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" ><i class="fad fa-at"></i></span>
</div>
<input type="email" name="mail" class="form-control" placeholder="jsmith@gmail.com" validate="email:3" />
</div>
</div>
<button type="submit" class="btn btn-outline-dark"><i class="fad fa-paper-plane"></i> Send It!</button>
</form>
</div>
</div>
</div>
</div> -->
<%- include('bottom') %>

View File

@ -3,19 +3,22 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>SSO Manager - Theta 42</title> <title>VPN-p2p - Theta 42</title>
<!-- CSS are placed here --> <!-- CSS are placed here -->
<!-- <link rel='stylesheet' href='/static/css/bootstrap.min.css' /> --> <!-- <link rel='stylesheet' href='/static/css/bootstrap.min.css' /> -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<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 src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.5.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.5.0.min.js"></script>
<!-- <script type="text/javascript" src='/static/js/jquery.min.js'></script> --> <!-- <script type="text/javascript" src='/static/js/jquery.min.js'></script> -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<!-- <script type="text/javascript" src='/static/js/bootstrap.min.js'></script> --> <!-- <script type="text/javascript" src='/static/js/bootstrap.min.js'></script> -->
<script src="https://kit.fontawesome.com/4625ee80a2.js" crossorigin="anonymous"></script> <script src="https://kit.fontawesome.com/4625ee80a2.js" crossorigin="anonymous"></script>
<script type="text/javascript" src='/static/js/mustache.min.js'></script> <script type="text/javascript" src='/static/js/mustache.min.js'></script>
<script src="//stuff.718it.biz/jq-repeat.js"></script>
<script type="text/javascript" src="/static/js/app.js"></script> <script type="text/javascript" src="/static/js/app.js"></script>
<script type="text/javascript" src="/static/js/val.js"></script> <script type="text/javascript" src="/static/js/val.js"></script>
<script type="text/javascript" src="/static/js/moment.js"></script> <script type="text/javascript" src="/static/js/moment.js"></script>
@ -28,11 +31,10 @@
</head> </head>
<body> <body>
<header class="shadow d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm"> <header class="shadow d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
<h5 class="hover-effect my-0 mr-md-auto font-weight-normal">SSO Manager - Theta 42</h5> <h5 class="hover-effect my-0 mr-md-auto font-weight-normal">VPN p2p - Theta 42</h5>
<nav class="my-2 my-md-0 mr-md-3"> <nav class="my-2 my-md-0 mr-md-3">
<a class="text-dark hover-effect" href="/"><i class="fad fa-tachometer-alt-fastest"></i> Home</a> <a class="text-dark hover-effect" href="/topics"><i class="fad fa-tachometer-alt-fastest"></i> Topics</a>
<a class="text-dark hover-effect" href="/users"><i class="fad fa-users"></i> Users</a> <a class="text-dark hover-effect" href="/chat"><i class="fad fa-tachometer-alt-fastest"></i> Chat</a>
<a class="text-dark hover-effect" href="/groups"><i class="fad fa-users-class"></i> Groups</a>
</nav> </nav>
<a class="hover-effect btn btn-outline-primary" onclick="app.auth.logOut(e => window.location.href='/')"><i class="fas fa-sign-out"></i> Log Out</a> <a class="hover-effect btn btn-outline-primary" onclick="app.auth.logOut(e => window.location.href='/')"><i class="fas fa-sign-out"></i> Log Out</a>

88
nodejs/views/topics.ejs Normal file
View File

@ -0,0 +1,88 @@
<%- include('top') %>
<script id="rowTemplate" type="text/html">
<p>
<div class="card shadow">
<div class="card-header">
<i class="fad fa-users-class"></i>
<b>Topic:</b> {{ topic }}
</div>
<div class="card-body">
{{ data }}
</div>
</div>
</p>
</script>
<script type="text/javascript">
var publishTopic = function(btn){
event.preventDefault(); // avoid to execute the actual submit of the form.
var $form = $(btn).closest( '[action]' ); // gets the 'form' parent
var formData = $form.find( '[name]' ).serializeObject();
if( !$form.validate()) {
app.util.actionMessage('Please fix the form errors.', $form, 'danger')
return false;
}
app.publish(formData.topic, JSON.parse(formData.data))
$form.trigger("reset");
app.util.actionMessage('Topic '+formData.topic+' published!', $form, 'success'); //re-populate table
};
$(document).ready(function(){
app.subscribe(/./g, function(data, topic){
var rowTemplate = $('#rowTemplate').html();
var $target = $('#tableAJAX');
user_row = Mustache.render(rowTemplate, {topic, data: JSON.stringify(data)});
$target.append(user_row);
$target.fadeIn('slow');
});
});
</script>
<div class="row" style="display:none">
<div class="col-md-4">
<div class="card shadow-lg">
<div class="card-header">
<i class="fas fa-layer-plus"></i>
Publish
</div>
<div class="card-header actionMessage" style="display:none"></div>
<div class="card-body">
<form action="group/" method="post" onsubmit="publishTopic(this)">
<div class="form-group">
<label class="control-label">Topic</label>
<input type="text" class="form-control shadow" name="topic" placeholder="app_gitea_admin" validate=":3" />
</div>
<div class="form-group">
<label class="control-label">JSON</label>
<textarea class="form-control shadow" name="data" placeholder="{...}" validate=":3"></textarea>
</div>
<button type="submit" class="btn btn-outline-dark">Publish</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow-lg">
<div class="card-header">
<i class="fad fa-users-class"></i>
Incoming Topics
</div>
<div class="card-header actionMessage" style="display:none">
</div>
<div class="card-body">
<div class="" id="tableAJAX">
</div>
</div>
</div>
</div>
</div>
<%- include('bottom') %>

491
package-lock.json generated
View File

@ -1,491 +0,0 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"bcrypt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-4.0.1.tgz",
"integrity": "sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ==",
"requires": {
"node-addon-api": "^2.0.0",
"node-pre-gyp": "0.14.0"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"requires": {
"minipass": "^2.6.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ignore-walk": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"requires": {
"minimatch": "^3.0.4"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"requires": {
"number-is-nan": "^1.0.0"
}
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"requires": {
"minipass": "^2.9.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"needle": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz",
"integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
}
},
"node-addon-api": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
"node-pre-gyp": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz",
"integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4.4.2"
}
},
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
},
"npm-bundled": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"requires": {
"npm-normalize-package-bin": "^1.0.1"
}
},
"npm-normalize-package-bin": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
},
"npm-packlist": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
},
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"requires": {
"string-width": "^1.0.2 || 2"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}
}
}