This commit is contained in:
William Mantly 2021-01-25 23:42:47 -05:00
parent 10d10079aa
commit e585683664
Signed by: wmantly
GPG Key ID: 186A8370EFF937CA
22 changed files with 3116 additions and 1268 deletions

View File

@ -33,13 +33,12 @@ app.use('/', require('./routes/index'));
// API routes for authentication. // API routes for authentication.
app.use('/api/auth', require('./routes/auth')); app.use('/api/auth', require('./routes/auth'));
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/token', middleware.auth, require('./routes/token'));
app.use('/api/group', middleware.auth, require('./routes/group'));
// 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
@ -54,7 +53,7 @@ app.use(function(req, res, next) {
// Error handler. This is where `next()` will go on error // Error handler. This is where `next()` will go on error
app.use(function(err, req, res, next) { app.use(function(err, req, res, next) {
console.error(err.status || res.status, err.name, req.method, req.url); console.error(err.status || res.status, err.name, req.method, req.url);
if(![401, 404].includes(err.status || res.status)){ if(![ 404].includes(err.status || res.status)){
console.error(err.message); console.error(err.message);
console.error(err.stack); console.error(err.stack);
console.error('========================================='); console.error('=========================================');

View File

@ -7,12 +7,13 @@
var app = require('../app'); var app = require('../app');
var debug = require('debug')('proxy-api:server'); var debug = require('debug')('proxy-api:server');
var http = require('http'); var http = require('http');
const conf = require('../conf/conf');
/** /**
* Get port from environment and store in Express. * Get port from environment and store in Express.
*/ */
var port = normalizePort(process.env.NODE_PORT || app.conf.port || '3000'); var port = normalizePort(process.env.NODE_PORT || conf.port || '3000');
app.set('port', port); app.set('port', port);
/** /**

View File

@ -3,13 +3,16 @@
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__', httpProxyAPI:{
host: 'http://10.1.0.51:3000',
key: '__IN SRECREST FILE__'
}
}; };

252
nodejs/lib/deploy.js Normal file
View File

@ -0,0 +1,252 @@
'user strict';
const extend = require('extend');
const axios = require('axios')
const {Repo, Environment, Deployment, Target} = require('../models/repo');
const deployTargets = require('./lxc');
const conf = require('../conf/conf')
async function doDeploy(action, repo, branch, repoSshurl, commit){
var deployment;
try{
console.log(action, repo, branch, repoSshurl, commit)
repo = await Repo.get(repo);
let deployments = await repo.getDeploymentsbyBranch(branch, true)
console.log('deployments', deployments)
if(deployments.length && action === 'delete'){
deployment = deployments[0]
}if(deployments.length){
deployment = deployments[0]
action = 'update';
}else{
var environment = await repo.getEnvironmentsbyBranch(branch)
deployment = await environment.addDeployment()
}
deployment.environment.settings.repoSshurl = repoSshurl
deployment.environment.settings.branch = branch
}catch(error){
console.error('create start', error)
throw new Error('Failed to make new Deployment')
}
try{
deployment = new Depoy(deployment);
setImmediate(async function(deployment, action) {
try{
await deployment[action]()
}catch(error){
console.log('set error', error)
}
}, deployment, action)
return {id: deployment.id};
}catch(error){
console.error('create remote', error)
}
}
function event(deployment, message){
console.info('event:', message)
}
class Depoy{
constructor(deployment){
this.deployment = deployment
this.environment = deployment.environment;
this.settings = pasrseSetings(deployment);
this.secrets = pasrseSecrets(deployment);
this.id = deployment.repo_env_id
this.target = new deployTargets[deployment.target.type](this.settings)
}
async exec(code, info){
await this.event(`exec-start`, {info})
code = `
sudo su
${exportBashVars(this.secrets)}
echo 'nameserver 8.8.8.8' > /etc/resolv.conf
export DEBIAN_FRONTEND=noninteractive
${code}
`
let res = await this.target.exec(code);
await this.event(`exec-finish`, {info, ...res})
return res;
}
async event(name, data){
console.log(`EVENT: ${name}`, data)
}
async log(type, message){
console.log('LOG:', type, message)
}
async setinfo(){
let info = await this.target.info();
if(!info.ip){
return await this.setinfo();
}
let id = info.ip.slice(-2);
let settings = {
sshURL: `${this.settings.host}:22${id}`,
httpURL: `${this.settings.host}:80${id}`,
}
this.settings = {...this.settings, ...settings};
await this.deployment.update('settings', {settings: this.settings, state:'deployed'})
}
async create(){
this.event('deployment-started', {info: `Creating deployment ${this.settings.appName}`})
await this.target.create('bionic-base')
await this.target.start()
await this.setinfo();
console.log(this.settings)
try{
await this.exec(`
while [ ! -f /opt/datacom/firstboot ]; do sleep 1; done
sleep 2
`, 'Wait for target to be ready')
}catch(error){}
await this.init();
await this.updateProxy();
}
async init(){
await this.exec(deployInitScript(this.settings), 'Initializing deployment')
await this.exec(`
cd ${this.settings.workingPath};
./${this.settings.scriptsPath}/${this.environment.environment}/deploy.sh
`, 'Running repo deploy script')
}
async update(){
await this.exec(`
cd ${this.settings.workingPath};
export GIT_SSH_COMMAND="/usr/bin/ssh -o StrictHostKeyChecking=no -i $HOME/.ssh/id_rsa_deploy_key"
git pull origin master;
./${this.settings.scriptsPath}/${this.environment.environment}/update.sh
`, 'Running repo update script')
}
async updateProxy(){
let target = this.settings.httpURL.split(':');
let res = await axios.post(`${conf.httpProxyAPI.host}/api/host/`, {
forcessl: true,
host: this.settings.domain.replace('*', this.settings.branch),
ip: target[0],
targetPort: Number(target[1] || 80),
targetssl: false
}, {
headers: { "auth-token": conf.httpProxyAPI.key }
})
}
async delete(){
await this.target.destroy()
await this.deployment.update({state: 'deleted', isActive: false})
}
}
function deployUpdateScript(argument) {
// body...
}
function deployInitScript(args){
return `
mkdir -p "${args.workingPath}";
mkdir "$HOME/.ssh";
chmod 700 "$HOME/.ssh"
echo "${args.privateKey}" > $HOME/.ssh/id_rsa_deploy_key
chmod 600 $HOME/.ssh/id_rsa_deploy_key
wget https://raw.githubusercontent.com/tests-always-included/mo/master/mo -O /usr/local/bin/mo
chmod +x /usr/local/bin/mo
export GIT_SSH_COMMAND="/usr/bin/ssh -o StrictHostKeyChecking=no -i $HOME/.ssh/id_rsa_deploy_key"
git clone ${args.repoSshurl} ${args.workingPath};
`
}
function exportBashVars(map){
let out = '';
for (const [key, value] of Object.entries(map)){
out += `export ${key}="${value}";`
}
return out
}
function pasrseBase(deployment){
let appName = deployment.repo_env_id.replace('/', '_')
return {
appName: appName,
scriptsPath: deployment.environment.repo.scriptsPath,
privateKey: deployment.environment.repo.privateKey,
environment: deployment.environment.environment,
workingPath: `${deployment.environment.workingPath}/${appName}`,
domain: deployment.environment.domain,
name: appName,
}
}
function pasrseSecrets(deployment){
return {
...deployment.environment.repo.secrets,
...deployment.environment.secrets,
...pasrseBase(deployment),
}
}
function pasrseSetings(deployment){
return {
...deployment.target.settings,
...deployment.environment.repo.settings,
...deployment.environment.settings,
...pasrseBase(deployment),
}
}
module.exports = {doDeploy};
(async function(){try{
// console.log(await doDeploy('create', 'wmantly/static-test', 'master', 'ssh://gitea@git.theta42.com:2222/wmantly/static-test.git'))
// let repo = await Repo.get('wmantly/static-test');
// let deployments = await repo.getDeploymentsbyBranch('master')
// for(let d of deployments){
// try{
// let lxc = new deployTargets.LXC({...{name: d.repo_env_id.replace('/', '_')}, ...d.target.settings})
// await lxc.destroy();
// await d.remove()
// }catch(error){
// console.log('err', error)
// }finally{
// await d.remove()
// }
// }
}catch(error){
console.error('IIFE error:', error)
}})()

174
nodejs/lib/lxc.js Normal file
View File

@ -0,0 +1,174 @@
'use strict';
const util = require('util');
const exec = util.promisify(require('child_process').exec)
class Local{
async sysExec(command){
try{
return await exec(command)
}catch(error){
throw(error);
}
}
async exec(...args){
await this.sysExec()
}
}
class SSH extends Local{
constructor(args){
super()
this.user = args.user;
this.host = args.host;
this.keyPath = args.keyPath;
}
async sysExec(command){try{
// console.log('command', command)
command = new Buffer.from(command).toString('base64');
command = `ssh -i "${this.keyPath}" -o StrictHostKeyChecking=no ${this.user}@${this.host} "echo ${command} | base64 --decode | bash"`;
return await super.sysExec(command);
}catch(error){
throw error;
}}
}
class LXC{
constructor(args){
// console.log('lxc args', args)
this.name = args.name
if(args.host){
this.sysExec = (new SSH(args)).sysExec.bind(args)
}else{
this.sysExec = (new Local()).sysExec
}
}
static async list(){
try{
let res = await this.prototype.sysExec(`lxc-ls --fancy`);
let output = res.stdout.split("\n").slice(0).slice(0,-1);
let keys = output.splice(0,1)[0].split(/\s+/).slice(0,-1).map(function(v){return v.toLowerCase()});
let info = [];
for(let line of output){
if(line.match(/^-/)) continue;
line = line.split(/\s+/).slice(0,-1);
let mapOut = {};
line.map(function(value,idx){
mapOut[keys[idx]] = value;
});
info.push(mapOut);
}
return info;
}catch(error){
throw error;
}
}
async create(from){
try{
return await this.sysExec(`lxc-copy --name "${from}" --newname "${this.name}" --daemon`);
}catch(error){
throw error;
}
}
async start(){
try{
return await this.sysExec(`lxc-start --name "${this.name}" --daemon`);
}catch(error){
throw error;
}
}
async destroy(){
try{
let res = await this.sysExec(`lxc-destroy --force --name ${this.name}`)
return !!res.stdout.match(/Destroyed container/);
}catch(error){
throw error;
}
}
async stop(){
try{
return await this.sysExec(`lxc-stop --name "${this.name}"`);
}catch(error){
throw error;
}
}
async exec(code){
try{
code = new Buffer.from(code).toString('base64')
return await this.sysExec(`lxc-attach -n "${this.name}" --clear-env -- bash -c 'echo "${code}" | base64 --decode | bash'`)
}catch(error){
throw error;
}
}
async info(){
try{
let info = {};
let res = await this.sysExec(`lxc-info --name "${this.name}"`);
res = res.stdout;
if(res.match("doesn't exist")){
throw new Error('ContainerDoesntExist')
}
res = res.replace(/\suse/ig, '').replace(/\sbytes/ig, '').split("\n").slice(0,-1);
for(var i in res){
var temp = res[i].split(/\:\s+/);
info[temp[0].toLowerCase().trim()] = temp[1].trim();
}
var args = [info].concat(Array.prototype.slice.call(arguments, 1));
return info;
}catch(error){
throw error;
}
}
async setAutoStart(name){
await this.sysExec(`echo "lxc.start.auto = 1" >> "$HOME/.local/share/lxc/${this.name}/config"`)
}
}
module.exports = {Local, SSH, LXC};
(async function(){try{
// let lxc = new LXC();
// // console.log(await lxc.copy('hass', 'hass2'))
// console.log(await lxc.destroy('hass2'))
// console.log(await LXC.list())
// let lxc = new LXC({name:'hass'})
// console.log(await hass.start())
// let lxc = new LXC({user:'virt-service', host:'142.93.30.52', keyPath:'/home/william/.ssh/id_rsa_virt-service', name: 'test2'})
// console.log(await lxc.exec('hostname'))
// // console.log(await lxc.exec('test2', 'sleep 50'))
// console.log(await lxc.info())
//
}catch(error){
console.error('IIFE error', error);
}})()

View File

@ -22,6 +22,7 @@ Auth.login = async function(data){
return {user, token} return {user, token}
}catch(error){ }catch(error){
console.log('login error', error);
throw this.errors.login(); throw this.errors.login();
} }
}; };

View File

@ -1,281 +0,0 @@
'use strict';
const { Client, Attribute, Change } = require('ldapts');
const conf = require('../app').conf.ldap;
const client = new Client({
url: conf.url,
});
async function getGroups(client, member){
try{
let memberFilter = member ? `(member=${member})`: ''
let groups = (await client.search(conf.groupBase, {
scope: 'sub',
filter: `(&(objectClass=groupOfNames)${memberFilter})`,
attributes: ['*', 'createTimestamp', 'modifyTimestamp'],
})).searchEntries;
return groups.map(function(group){
if(!Array.isArray(group.member)) group.member = [group.member];
if(!Array.isArray(group.owner)) group.owner = [group.owner];
return group
});
}catch(error){
throw error;
}
}
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;
}
}
async function addOwner(client, group, user){
try{
await client.modify(group.dn, [
new Change({
operation: 'add',
modification: new Attribute({
type: 'owner',
values: [user.dn]
})
}),
]);
}catch(error){
// if(error = "TypeOrValueExistsError"){
// console.error('addMember error skipped', error)
// return ;
// }
throw error;
}
}
async function removeOwner(client, group, user){
try{
await client.modify(group.dn, [
new Change({
operation: 'delete',
modification: new Attribute({
type: 'owner',
values: [user.dn]
})}),
]);
}catch(error){
if(error = "TypeOrValueExistsError")return ;
throw error;
}
}
var Group = {};
Group.list = async function(member){
try{
await client.bind(conf.bindDN, conf.bindPassword);
let groups = await getGroups(client, member)
await client.unbind();
return groups.map(group => group.cn);
}catch(error){
throw error;
}
}
Group.listDetail = async function(member){
try{
await client.bind(conf.bindDN, conf.bindPassword);
let groups = await getGroups(client, member)
await client.unbind();
return groups;
}catch(error){
throw error;
}
}
Group.get = async function(data){
try{
if(typeof data !== 'object'){
let name = data;
data = {};
data.name = name;
}
await client.bind(conf.bindDN, conf.bindPassword);
let group = (await client.search(conf.groupBase, {
scope: 'sub',
filter: `(&(objectClass=groupOfNames)(cn=${data.name}))`,
attributes: ['*', 'createTimestamp', 'modifyTimestamp'],
})).searchEntries[0];
await client.unbind();
if(!Array.isArray(group.member)) group.member = [group.member];
if(!Array.isArray(group.owner)) group.owner = [group.owner];
if(group){
let obj = Object.create(this);
Object.assign(obj, group);
return obj;
}else{
let error = new Error('GroupNotFound');
error.name = 'GroupNotFound';
error.message = `LDAP:${data.cn} does not exists`;
error.status = 404;
throw error;
}
}catch(error){
throw error;
}
}
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.addOwner = async function(user){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await addOwner(client, this, user);
await client.unbind();
return this;
}catch(error){
throw error;
}
};
Group.removeOwner = async function(user){
try{
await client.bind(conf.bindDN, conf.bindPassword);
await removeOwner(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};

258
nodejs/models/repo.js Normal file
View File

@ -0,0 +1,258 @@
'use strict';
const {promisify} = require('util');
const forge = require('node-forge');
const Table = require('../utils/redis_model');
var rasGenerate = promisify(forge.pki.rsa.generateKeyPair);
async function generateOpenSshPair(keySize){
keySize = keySize || 2048;
let keyPair = await rasGenerate({bits: keySize});
return {
publicKey: forge.ssh.publicKeyToOpenSSH(keyPair.publicKey),
privateKey: forge.ssh.privateKeyToOpenSSH(keyPair.privateKey)
};
};
const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};
class Repo extends Table{
static _key = 'repo'
static _keyMap = {
'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
'created_on': {default: function(){return (new Date).getTime()}},
'updated_by': {default:"__NONE__", isRequired: false, type: 'string',},
'updated_on': {default: function(){return (new Date).getTime()}, always: true},
'repo': {isRequired: true, type: 'string', min: 3, max: 500},
'hookCallCount': {default: 0, type: 'number'},
'scriptsPath': {default:'scripts', type: 'string'},
'settings': {default: {}, type:'object'},
'secrets': {default: {}, type: 'object', min: 3, max: 500},
'privateKey': {type: 'string'},
'publicKey': {type: 'string'},
}
constructor(...args){
super(...args);
}
static async add(data){
return super.add({...data, ...(await generateOpenSshPair(2048))})
}
async getEnvironments(){
let environments = await Environment.list();
let out = [];
for(let environment of environments){
if(environment.startsWith(this.repo)){
environment = await Environment.get(environment);
environment.repo = this;
out.push(environment)
}
}
return out;
}
async getEnvironmentsbyBranch(branch){
let list = await this.getEnvironments();
let any;
for(let key of list){
if(branch === key.branchMatch) return key;
if(key.branchMatch === '*') any = key;
}
return any;
}
async getDeploymentsbyBranch(branch, state){
let environment = await this.getEnvironmentsbyBranch(branch);
let deployments = await Deployment.list();
let out = []
for(let deployment of deployments){
if(deployment.startsWith(`${this.repo}_${environment.environment}`)){
deployment = await Deployment.get(deployment);
deployment.environment = environment;
deployment.target = await Target.get(environment.target);
out.push(deployment)
if(state && deployment.state === state){
}
}
}
return out;
}
}
class Environment extends Table{
static _key = 'repo_env'
static _keyMap = {
'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
'created_on': {default: function(){return (new Date).getTime()}},
'updated_by': {default:"__NONE__", isRequired: false, type: 'string',},
'updated_on': {default: function(){return (new Date).getTime()}, always: true},
'repo_env': {isRequired: true, type: 'string', min: 3, max: 500},
'repo': {type: 'string', min: 3, max: 500},
'environment': {isRequired: true, type: 'string', min: 3, max: 500},
'branchMatch': {isRequired: true, type: 'string', min: 1, max: 500},
'target': {isRequired: true, type: 'string', min: 3, max: 500},
'settings': {default: {}, type: 'object', min: 3, max: 500},
'secrets': {default: {}, type: 'object', min: 3, max: 500},
'hookCallCount': {default: 0, type: 'number'},
'lastCommit': {default:"__NONE__", isRequired: false, type: 'string'},
'workingPath': {default: '/opt/datacom', type: 'string'},
'domain': {isRequired: true, type: 'string'},
}
static async add(data){
try{
await Repo.get(data.repo);
await Target.get(data.target);
data.repo_env = `${data.repo}_${data.environment}`
return await super.add(data);
}catch(error){
throw error;
}
};
async addDeployment(data){
try{
data = data || {}
data.created_by = data.uid || this.created_by;
data.repo = this.repo.repo || this.repo;
data.environment = this.environment;
data.id = UUID().split('-').reverse()[0]
data.repo_env_id = `${data.repo}_${data.environment}_${data.id}`
let deployment = await Deployment.add(data);
deployment.target = await Target.get(this.target)
deployment.environment = this;
return deployment;
}catch(error){
throw error;
}
};
}
class Deployment extends Table{
static _key = 'repo_env_id'
static _keyMap = {
'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
'created_on': {default: function(){return (new Date).getTime()}},
'updated_by': {default:"__NONE__", isRequired: false, type: 'string',},
'updated_on': {default: function(){return (new Date).getTime()}, always: true},
'id': {type: 'string', min: 12, max: 12},
'repo_env_id': {isRequired: true, type: 'string', min: 3, max: 500},
'repo': {type: 'string', min: 3, max: 500},
'environment': {isRequired: true, type: 'string', min: 3, max: 500},
'state': {default: 'new', type: 'string', min: 3, max: 500},
'isActive': {default: true, type: 'boolean',},
'target_url': {default:"__NONE__", isRequired: false, type: 'string'},
}
}
class Target extends Table{
static _key = 'name'
static _keyMap = {
'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
'created_on': {default: function(){return (new Date).getTime()}},
'updated_by': {default:"__NONE__", isRequired: false, type: 'string',},
'updated_on': {default: function(){return (new Date).getTime()}, always: true},
'name': {isRequired: true, type: 'string', min: 2, max: 500},
'type': {isRequired: true, type: 'string', min: 1, max: 36},
'settings': {default: {}, type: 'object', min: 3, max: 500},
}
}
module.exports = {Repo, Environment, Deployment, Target};
(async function(){try{
// // console.log(await Repo.list())
// // To ssh://git.theta42.com:2222/wmantly/static-test.git
// let lxc_starting = await Target.add({
// created_by: 'wmantly',
// name: 'lxc_starting',
// type: 'LXC',
// settings: {
// user:'virt-service',
// host:'142.93.30.52',
// keyPath:'/home/william/.ssh/id_rsa_virt-service'
// }
// });
// var repo = await Repo.add({
// created_by: 'wmantly',
// repo: 'wmantly/static-test',
// })
// var environment = await Environment.add({
// created_by: 'wmantly',
// environment: 'staging',
// branchMatch: '*',
// repo: 'wmantly/static-test',
// domain: 'test.dc.vm42.us',
// target: 'lxc_starting'
// })
let environment = await Environment.get('wmantly/static-test_staging')
await environment.update({'domain': '*.dc.vm42.us'})
// // console.log(test)
// // console.log(await Environment.listDetail())
// // let repo = await Repo.get('wmantly/test2')
// // console.log(repo)
// // repo.update({hookCallCount: 5});
// // let envs = await repo.getEnvironments();
// // let env = await repo.getEnvironmentsbyBranch('staging');
// // let deployment = await env.addDeployment()
// // console.log('deployment', deployment)
// // let deployments = await repo.getDeploymentsbyBranch('staging')
// // console.log('deployments', deployments)
// // console.log('deployments', await Deployment.listDetail())
// console.log('repo', await Repo.listDetail())
// console.log('environment', await Environment.listDetail())
// for(let d of await Deployment.listDetail()){
// console.log('to remove', d)
// await d.remove()
// }
// console.log('deployment', await Deployment.listDetail())
// console.log('blah')
// let repo = await Repo.get('wmantly/static-test');
// // let environment = await repo.getEnvironmentsbyBranch('master')
// // console.log('environment', environment)
// let deployment = await repo.getDeploymentsbyBranch('master')
// console.log('deployments', deployment)
// return 0;
}catch(error){
console.error('IIFE error', error, error.message);
}})()

55
nodejs/models/test Normal file
View File

@ -0,0 +1,55 @@
var that
class Base{
static add(){
}
constructor(){
}
blah(){
that = this
}
}
class Ex extends Base{
static thingy = {a:1, b:2}
constructor(){
super()
}
}
Repo.byBranch = async function(repo){
let list = await Environment.list();
let out = [];
for(let key of list){
if(key.startsWith((repo || this.repo))) out.push(await Environment.get(key))
}
return out;
}
Environment.addDeployment = async function(data){
try{
data.repo = this.repo;
data.environment = this.environment;
data.id = UUID().split('-').reverse()[0]
data.repo_env = `${data.repo}_${data.environment}_${data.id}`
return await Deployment.__proto__.add.call(Environment, data);
}catch(error){
throw error;
}
};
module.exports = {Repo, Environment, Deployment, Target};
*/

View File

@ -1,71 +1,28 @@
'use strict'; 'use strict';
const redis_model = require('../utils/redis_model') const Table = require('../utils/redis_model')
const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}; const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};
class Token extends Table{
const Token = function(data){ static _key = 'token'
return redis_model({ static _keyMap = {
_name: `token_${data.name}`,
_key: 'token',
_keyMap: Object.assign({}, {
'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, 'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
'created_on': {default: function(){return (new Date).getTime()}}, 'created_on': {default: function(){return (new Date).getTime()}},
'updated_on': {default: function(){return (new Date).getTime()}, always: true}, 'updated_on': {default: function(){return (new Date).getTime()}, always: true},
'token': {default: UUID, type: 'string', min: 36, max: 36}, 'token': {default: UUID, type: 'string', min: 36, max: 36},
'is_valid': {default: true, type: 'boolean'} 'is_valid': {default: true, type: 'boolean'}
}, data.keyMap || {}) }
});
};
Token.check = async function(data){ async check(){
try{ return this.is_valid
return this.is_valid;
}catch(error){
return false
} }
} }
var InviteToken = Object.create(Token({ class AuthToken extends Token{
name: 'invite', static async add(data){
keyMap:{ data.created_by = data.created_by || data.uid;
claimed_by: {default:"__NONE__", isRequired: false, type: 'string',}, return await super.add(data)
mail: {default:"__NONE__", isRequired: false, type: 'string',},
mail_token: {default: UUID, type: 'string', min: 36, max: 36},
}
}));
InviteToken.consume = async function(data){
try{
if(this.is_valid){
data['is_valid'] = false;
await this.update(data);
return true;
}
return false;
}catch(error){
throw error;
} }
} }
var AuthToken = Object.create(Token({ module.exports = {Token, AuthToken};
name: 'auth',
}));
AuthToken.add = async function(data){
data.created_by = data.uid;
return AuthToken.__proto__.add(data);
};
var PasswordResetToken = Object.create(Token({
name: 'auth',
}));
PasswordResetToken.add = async function(data){
data.created_by = data.uid;
return PasswordResetToken.__proto__.add(data);
};
module.exports = {Token, InviteToken, AuthToken, PasswordResetToken};

View File

@ -1,116 +1,12 @@
'use strict'; 'use strict';
const { Client, Attribute, Change } = require('ldapts'); const {Client, Attribute} = require('ldapts');
const crypto = require('crypto');
const {Mail} = require('./email');
const {Token, InviteToken, PasswordResetToken} = require('./token');
const conf = require('../app').conf.ldap; const conf = require('../app').conf.ldap;
const client = new Client({ 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}`.toLowerCase();
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,198 +121,9 @@ 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);
await client.bind(user.dn, data.password); await client.bind(user.dn, data.password);

2232
nodejs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,15 +12,14 @@
"start": "node ./bin/www" "start": "node ./bin/www"
}, },
"dependencies": { "dependencies": {
"@sendgrid/mail": "^7.4.0", "axios": "^0.21.1",
"ejs": "^3.1.5", "ejs": "^3.1.5",
"express": "~4.16.1", "express": "~4.16.1",
"extend": "^3.0.2", "extend": "^3.0.2",
"ldapts": "^2.10.1", "ldapts": "^2.10.1",
"moment": "^2.29.1", "moment": "^2.29.1",
"mustache": "^4.1.0", "mustache": "^4.1.0",
"redis": "^2.8.0", "redis": "^2.8.0"
"smtpc": "^0.1.2"
}, },
"license": "MIT", "license": "MIT",
"repository": { "repository": {

View File

@ -3,18 +3,18 @@
const router = require('express').Router(); const router = require('express').Router();
const {User} = require('../models/user'); 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){
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,
message:`${req.body.uid} logged in!`,
}); });
}catch(error){ }catch(error){
console.log('error route', error)
next(error); next(error);
} }
}); });
@ -31,46 +31,14 @@ router.all('/logout', async function(req, res, next){
} }
}); });
router.post('/resetpassword', async function(req, res, next){ router.post('/invite/:token', 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{ try{
req.body.token = req.params.token; req.body.token = req.params.token;
req.body.mailToken = req.params.mailToken;
let user = await User.addByInvite(req.body); let user = await User.addByInvite(req.body);
let token = await AuthToken.add(user); let token = await AuthToken.add(user);
return res.json({ return res.json({
user: user.uid, user: user.username,
token: token.token token: token.token
}); });
@ -80,21 +48,6 @@ router.post('/invite/:token/:mailToken', async function(req, res, next) {
}); });
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;
/* /*

View File

@ -0,0 +1,28 @@
'use strict';
const router = require('express').Router();
const {doDeploy} = require('../lib/deploy');
router.all('/', async function(req, res, next) {
try{
var event = req.headers['x-github-event'];
var call = (req.body.created && 'create') ||
(req.body.deleted && 'delete') ||
'update';
var branch = req.body.ref.replace('refs/heads/', '');
var sshURL = req.body.repository.ssh_url;
var commit = req.body.after;
let repo = req.body.repository.full_name;
let id = await doDeploy('create', repo, branch, sshURL, commit);
res.json({id});
}catch(error){
next(error)
}
});
module.exports = router;

View File

@ -1,125 +0,0 @@
'use strict';
const router = require('express').Router();
const {User} = require('../models/user_ldap');
const {Group} = require('../models/group_ldap');
const permission = require('../utils/permission');
router.get('/', async function(req, res, next){
try{
let member = req.query.member ? await User.get(req.query.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{
await permission.byGroup(req.user, ['app_sso_admin']);
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('/owner/:group/:uid', async function(req, res, next){
try{
await permission.byGroup(req.user, ['app_sso_admin'], [req.params.group]);
var group = await Group.get(req.params.group);
var user = await User.get(req.params.uid);
return res.json({
results: group.addOwner(user),
message: `Added owner ${req.params.uid} to ${req.params.group} group.`
});
}catch(error){
next(error);
}
});
router.delete('/owner/:group/:uid', async function(req, res, next){
try{
await permission.byGroup(req.user, ['app_sso_admin'], [req.params.group]);
var group = await Group.get(req.params.group);
var user = await User.get(req.params.uid);
return res.json({
results: group.removeOwner(user),
message: `Removed Owner ${req.params.uid} from ${req.params.group} group.`
});
}catch(error){
next(error);
}
});
router.put('/:group/:uid', async function(req, res, next){
try{
await permission.byGroup(req.user, ['app_sso_admin'], [req.params.group]);
var group = await Group.get(req.params.group);
var user = await User.get(req.params.uid);
return res.json({
results: group.addMember(user),
message: `Added user ${req.params.uid} to ${req.params.group} group.`
});
}catch(error){
next(error);
}
});
router.delete('/:group/:uid', async function(req, res, next){
try{
await permission.byGroup(req.user, ['app_sso_admin'], [req.params.group]);
var group = await Group.get(req.params.group);
var user = await User.get(req.params.uid);
return res.json({
results: group.removeMember(user),
message: `Removed user ${req.params.uid} from ${req.params.group} group.`
});
}catch(error){
next(error);
}
});
router.delete('/:group', async function(req, res, next){
try{
await permission.byGroup(req.user, ['app_sso_admin'], [req.params.group]);
var group = await Group.get(req.params.group);
return res.json({
removed: await group.remove(),
results: group,
message: `Group ${req.params.group} Deleted`
});
}catch(error){
next(error);
}
});
module.exports = router;

View File

@ -2,124 +2,42 @@
const router = require('express').Router(); const router = require('express').Router();
const {User} = require('../models/user'); const {User} = require('../models/user');
const permission = require('../utils/permission'); const {Auth, AuthToken} = require('../models/auth');
router.get('/', async function(req, res, next){
router.post('/login', async function(req, res, next){
try{ try{
let auth = await Auth.login(req.body);
return res.json({ return res.json({
results: await User[req.query.detail ? "listDetail" : "list"]() login: true,
token: auth.token.token,
}); });
}catch(error){ }catch(error){
next(error); next(error);
} }
}); });
router.post('/', async function(req, res, next){ router.all('/logout', async function(req, res, next){
try{ try{
await permission.byGroup(req.user, ['app_sso_admin']) if(req.user){
await req.user.logout();
}
req.body.created_by = req.user.uid res.json({message: 'Bye'})
return res.json({results: await User.add(req.body)});
}catch(error){ }catch(error){
next(error); next(error);
} }
}); });
router.delete('/:uid', async function(req, res, next){ router.post('/invite/:token', async function(req, res, next) {
try{ try{
let user; req.body.token = req.params.token;
let user = await User.addByInvite(req.body);
if(req.params.uid.toLowerCase() === req.user.uid.toLowerCase()){ let token = await AuthToken.add(user);
user = req.user;
}else{
user = await User.get(req.params.uid);
await permission.byGroup(req.user, ['app_sso_admin'])
}
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;
if(req.params.uid.toLowerCase() === req.user.uid.toLowerCase()){
user = req.user;
}else{
user = await User.get(req.params.uid);
await permission.byGroup(req.user, ['app_sso_admin'])
}
return res.json({ return res.json({
results: await user.update(req.body), user: user.username,
message: `Updated ${req.params.uid} user` token: token.token
});
}catch(error){
next(error);
}
});
router.get('/me', async function(req, res, next){
try{
return res.json(await User.get({uid: req.user.uid}));
}catch(error){
next(error);
}
});
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;
if(req.params.uid.toLowerCase() === req.user.uid.toLowerCase()){
user = req.user;
}else{
user = await User.get(req.params.uid);
await permission.byGroup(req.user, ['app_sso_admin'])
}
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){ }catch(error){
@ -128,14 +46,22 @@ router.post('/key', async function(req, res, next){
}); });
router.get('/:uid', async function(req, res, next){
try{
return res.json({
results: await User.get(req.params.uid),
});
}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

@ -8,7 +8,7 @@ const process_type = {
string: function(key, value){ string: function(key, value){
if(key.min && value.length < key.min) return `is too short, min ${key.min}.` if(key.min && value.length < key.min) return `is too short, min ${key.min}.`
if(key.max && value.length > key.max) return `is too short, max ${key.max}.` if(key.max && value.length > key.max) return `is too short, max ${key.max}.`
} },
} }
function returnOrCall(value){ function returnOrCall(value){
@ -20,6 +20,7 @@ function processKeys(map, data, partial){
let out = {}; let out = {};
for(let key of Object.keys(map)){ for(let key of Object.keys(map)){
if(!map[key].always && partial && !data.hasOwnProperty(key)) continue; if(!map[key].always && partial && !data.hasOwnProperty(key)) continue;
if(!partial && map[key].isRequired && !data.hasOwnProperty(key)){ if(!partial && map[key].isRequired && !data.hasOwnProperty(key)){
@ -57,6 +58,7 @@ function parseFromString(map, data){
boolean: function(value){ return value === 'false' ? false : true }, boolean: function(value){ return value === 'false' ? false : true },
number: Number, number: Number,
string: String, string: String,
object: JSON.parse
}; };
for(let key of Object.keys(data)){ for(let key of Object.keys(data)){
@ -68,6 +70,14 @@ function parseFromString(map, data){
return data; return data;
} }
function parseToString(data){
let types = {
object: JSON.stringify
}
return (types[typeof(data)] || String)(data);
}
function ObjectValidateError(message) { function ObjectValidateError(message) {
this.name = 'ObjectValidateError'; this.name = 'ObjectValidateError';
this.message = (message || {}); this.message = (message || {});
@ -77,4 +87,4 @@ function ObjectValidateError(message) {
ObjectValidateError.prototype = Error.prototype; ObjectValidateError.prototype = Error.prototype;
module.exports = {processKeys, parseFromString, ObjectValidateError}; module.exports = {processKeys, parseFromString, ObjectValidateError, parseToString};

View File

@ -4,7 +4,7 @@ const {createClient} = require('redis');
const {promisify} = require('util'); const {promisify} = require('util');
const config = { const config = {
prefix: 'sso_' prefix: 'deploy_'
} }
function client() { function client() {
@ -13,6 +13,9 @@ function client() {
const _client = client(); const _client = client();
const SCAN = promisify(_client.SCAN).bind(_client);
module.exports = { module.exports = {
client: client, client: client,
HGET: promisify(_client.HGET).bind(_client), HGET: promisify(_client.HGET).bind(_client),
@ -24,4 +27,18 @@ module.exports = {
HGETALL: promisify(_client.HGETALL).bind(_client), HGETALL: promisify(_client.HGETALL).bind(_client),
SMEMBERS: promisify(_client.SMEMBERS).bind(_client), SMEMBERS: promisify(_client.SMEMBERS).bind(_client),
RENAME: promisify(_client.RENAME).bind(_client), RENAME: promisify(_client.RENAME).bind(_client),
HSCAN: promisify(_client.HSCAN).bind(_client),
SCAN: async function(match){
let coursor = 0;
let results = [];
do{
let res = await SCAN(coursor, 'MATCH', config.prefix+match);
coursor = Number(res[0]);
results.push(...res[1].map(e => e.replace(config.prefix, '')))
} while(coursor);
return results
}
}; };

View File

@ -4,54 +4,39 @@ const client = require('../utils/redis');
const objValidate = require('../utils/object_validate'); const objValidate = require('../utils/object_validate');
let table = {}; class Table{
constructor(data){
table.get = async function(data){ for(let key in data){
try{ this[key] = data[key];
// if the data argument was passed as the index key value, make a data }
// object and add the index key to it.
if(typeof data !== 'object'){
let key = data;
data = {};
data[this._key] = key;
} }
// Get all the hash keys for the passed index key. static async get(index){
let res = await client.HGETALL(`${this._name}_${data[this._key]}`); try{
// If the redis query resolved to something, prepare the data. let result = await client.HGETALL(`${this.prototype.constructor.name}_${index}`);
if(res){
if(!result){
let error = new Error('EntryNotFound');
error.name = 'EntryNotFound';
error.message = `${this.prototype.constructor.name}:${index} does not exists`;
error.status = 404;
throw error;
}
// Redis always returns strings, use the keyMap schema to turn them // Redis always returns strings, use the keyMap schema to turn them
// back to native values. // back to native values.
res = objValidate.parseFromString(this._keyMap, res); result = objValidate.parseFromString(this._keyMap, result);
// Make sure the index key in in the returned object. return new this.prototype.constructor(result)
res[this._key] = data[this._key];
// Create a instance for this redis entry.
var entry = Object.create(this);
// Insert the redis response into the instance.
Object.assign(entry, res);
// Return the instance to the caller.
return entry;
}
}catch(error){ }catch(error){
throw error throw error;
} }
let error = new Error('EntryNotFound'); }
error.name = 'EntryNotFound';
error.message = `${this._name}:${data[this._key]} does not exists`;
error.status = 404;
throw error;
};
table.exists = async function(data){ static async exists(index){
// Return true or false if the requested entry exists ignoring error's.
try{ try{
await this.get(data); await this.get(data);
@ -59,20 +44,19 @@ table.exists = async function(data){
}catch(error){ }catch(error){
return false; return false;
} }
}; }
table.list = async function(){ static async list(){
// return a list of all the index keys for this table. // return a list of all the index keys for this table.
try{ try{
return await client.SMEMBERS(this.prototype.constructor.name);
return await client.SMEMBERS(this._name);
}catch(error){ }catch(error){
throw error; throw error;
} }
}; }
table.listDetail = async function(){ static async listDetail(){
// Return a list of the entries as instances. // Return a list of the entries as instances.
let out = []; let out = [];
@ -81,12 +65,11 @@ table.listDetail = async function(){
} }
return out return out
}; }
table.add = async function(data){ static async add(data){
// Add a entry to this redis table. // Add a entry to this redis table.
try{ try{
// Validate the passed data by the keyMap schema. // Validate the passed data by the keyMap schema.
data = objValidate.processKeys(this._keyMap, data); data = objValidate.processKeys(this._keyMap, data);
@ -95,18 +78,18 @@ table.add = async function(data){
if(data[this._key] && await this.exists(data)){ if(data[this._key] && await this.exists(data)){
let error = new Error('EntryNameUsed'); let error = new Error('EntryNameUsed');
error.name = 'EntryNameUsed'; error.name = 'EntryNameUsed';
error.message = `${this._name}:${data[this._key]} already exists`; error.message = `${this.prototype.constructor.name}:${data[this._key]} already exists`;
error.status = 409; error.status = 409;
throw error; throw error;
} }
// Add the key to the members for this redis table // Add the key to the members for this redis table
await client.SADD(this._name, data[this._key]); await client.SADD(this.prototype.constructor.name, data[this._key]);
// Add the values for this entry. // Add the values for this entry.
for(let key of Object.keys(data)){ for(let key of Object.keys(data)){
await client.HSET(`${this._name}_${data[this._key]}`, key, data[key]); await client.HSET(`${this.prototype.constructor.name}_${data[this._key]}`, key, objValidate.parseToString(data[key]));
} }
// return the created redis entry as entry instance. // return the created redis entry as entry instance.
@ -114,18 +97,13 @@ table.add = async function(data){
} catch(error){ } catch(error){
throw error; throw error;
} }
}; }
table.update = async function(data, key){ async update(data, key){
// Update an existing entry. // Update an existing entry.
try{ try{
// If an index key is passed, we assume is passed, assume we are not
// part of an entry instance. Make one and recall this from from a entry
// instance,
if(key) return await (await this.get(key)).update(data);
// Check to see if entry name changed. // Check to see if entry name changed.
if(data[this._key] && data[this._key] !== this[this._key]){ if(data[this.constructor._key] && data[this.constructor._key] !== this[this.constructor._key]){
// Merge the current data into with the updated data // Merge the current data into with the updated data
let newData = Object.assign({}, this, data); let newData = Object.assign({}, this, data);
@ -141,12 +119,12 @@ table.update = async function(data, key){
// Update what ever fields that where passed. // Update what ever fields that where passed.
// Validate the passed data, ignoring required fields. // Validate the passed data, ignoring required fields.
data = objValidate.processKeys(this._keyMap, data, true); data = objValidate.processKeys(this.constructor._keyMap, data, true);
// Loop over the data fields and apply them to redis // Loop over the data fields and apply them to redis
for(let key of Object.keys(data)){ for(let key of Object.keys(data)){
this[key] = data[key]; this[key] = data[key];
await client.HSET(`${this._name}_${this[this._key]}`, key, data[key]); await client.HSET(`${this.constructor.name}_${this[this.constructor._key]}`, key, data[key]);
} }
} }
@ -156,18 +134,18 @@ table.update = async function(data, key){
// Pass any error to the calling function // Pass any error to the calling function
throw error; throw error;
} }
}; }
table.remove = async function(data){ async remove(data){
// Remove an entry from this table. // Remove an entry from this table.
data = data || this;
try{ try{
// Remove the index key from the tables members list. // Remove the index key from the tables members list.
await client.SREM(this._name, data[this._key]);
await client.SREM(this.constructor.name, this[this.constructor._key]);
// Remove the entries hash values. // Remove the entries hash values.
let count = await client.DEL(`${this._name}_${data[this._key]}`); let count = await client.DEL(`${this.constructor.name}_${this[this.constructor._key]}`);
// Return the number of removed values to the caller. // Return the number of removed values to the caller.
return count; return count;
@ -177,14 +155,7 @@ table.remove = async function(data){
} }
}; };
function Table(data){ }
// Create a table instance.
let instance = Object.create(data);
Object.assign(instance, table);
// Return the table instance to the caller.
return Object.create(instance);
};
module.exports = Table; module.exports = Table;

View File

@ -16,7 +16,7 @@
<div class="card-deck"> <div class="card-deck">
<div class="shadow-lg card mb-3"> <div class="shadow-lg card mb-3">
<div class="card-header shadow"> <div class="card-header shadow">
Password Log in SSO Log in
</div> </div>
<div class="card-header shadow actionMessage" style="display:none"> <div class="card-header shadow actionMessage" style="display:none">
</div> </div>
@ -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') %>

View File

@ -3,7 +3,7 @@
<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>Deployment Manager - Theta 42</title>
<!-- CSS are placed here --> <!-- CSS are placed here -->
<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' />
@ -25,7 +25,7 @@
</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">Deployment Manager - 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="/"><i class="fad fa-tachometer-alt-fastest"></i>Home</a>
<a class="text-dark hover-effect" href="/users"><i class="fad fa-users"></i>Users</a> <a class="text-dark hover-effect" href="/users"><i class="fad fa-users"></i>Users</a>