diff --git a/nodejs/controller/auth.js b/nodejs/controller/auth.js index 0749b98..d117173 100644 --- a/nodejs/controller/auth.js +++ b/nodejs/controller/auth.js @@ -1,7 +1,7 @@ 'use strict'; -const {User} = require('../models/user'); -const {AuthToken} = require('../models/token'); +const Table = require('../models'); +const {User, AuthToken} = Table.models; class Auth{ @@ -35,6 +35,7 @@ class Auth{ throw this.errors.login(); }catch(error){ + console.log('check error', error); throw this.errors.login(); } } diff --git a/nodejs/middleware/auth.js b/nodejs/middleware/auth.js index 49c2ca7..802d7d1 100755 --- a/nodejs/middleware/auth.js +++ b/nodejs/middleware/auth.js @@ -5,7 +5,7 @@ const {Auth} = require('../controller/auth'); async function auth(req, res, next){ try{ req.token = await Auth.checkToken(req.header('auth-token')); - req.user = await req.token.getUser(); + req.user = req.token.user; return next(); }catch(error){ next(error); @@ -15,7 +15,7 @@ async function auth(req, res, next){ async function authIO(socket, next){ try{ let token = await Auth.checkToken(socket.handshake.auth.token || 0); - socket.user = await token.getUser(); + socket.user = token.user; next(); }catch(error){ next(error); diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js new file mode 100644 index 0000000..dd8bb5c --- /dev/null +++ b/nodejs/models/dns_provider.js @@ -0,0 +1,204 @@ +'use strict'; + +const crypto = require("crypto"); + +const conf = require('../conf'); +const Table = require('../utils/redis_model'); +const ModelPs = require('../utils/model_pubsub'); + +const tldExtract = require('tld-extract').parse_host; + +const providers = { + Cloudflare: require('./dns_provider/cloudflare'), + DigitalOcean: require('./dns_provider/digitalocean'), + PorkBun: require('./dns_provider/porkbun'), +}; + +class Domain extends Table{ + static _key = 'domain'; + 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}, + 'domain': {isRequired: true, type: 'string'}, + 'dnsProvider_id': {isRequired: true, type: 'string'}, + 'provider': {model: 'DnsProvider', rel:'one', localKey: 'dnsProvider_id'}, + 'zoneId': {isRequired: false, type: 'string'}, + } + + static async get(domain, ...args){ + try{ + domain = tldExtract(domain).domain; + }catch{} + + return await super.get(domain, ...args); + } + + async getRecords(...args){ + return await this.provider.api.getRecords(this, ...args); + } + + async createRecord(...args){ + return await this.provider.api.createRecord(this, ...args); + } + + async deleteRecords(...args){ + return await this.provider.api.deleteRecords(this, ...args); + } +} + +Domain.register(ModelPs(Domain)); + +class DnsProvider extends Table{ + static _key = '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': {default: ()=>crypto.randomBytes(8).toString("hex")}, + 'name': {isRequired: true, type: 'string'}, + 'dnsProvider': {isRequired: true, type: 'string'}, + 'domains': {model:'Domain', rel: 'many', remoteKey: 'dnsProvider_id'} + } + + static __intraModel(provider){ + if(!Object.keys(providers).includes(provider)){ + throw new Error('Invalid DNS provider'); + } + + let Provider = providers[provider]; + let _keyMap = {...this._keyMap, ...Provider._keyMap}; + + return ({ + [this.name] : class extends this { + static _keyMap = _keyMap; + static Provider = Provider; + } + })[this.name]; + } + + static async create(data, ...args){ + let Provider; + try{ + let __intraModel = this.__intraModel(data.dnsProvider); + Provider = __intraModel.Provider; + + // This is here test if the given API key is valid + let provider = new __intraModel.Provider(data, ...args); + let domains = await provider.listDomains(); + + let instance = await super.create.call(__intraModel, data, ...args); + await instance.updateDomains(domains); + + return instance; + }catch(error){ + if(error.name === 'UnauthorizedDnsApi'){ + let keys = []; + console.log('Provider', Provider) + for(let key in Provider._keyMap){ + keys.push({'key': key, message: 'Invalid Key'}) + } + throw this.errors.ObjectValidateError(keys, "API rejected key"); + } + } + } + + static async get(data, ...args){ + let instance = await super.get(data, ...args); + let __intraModel = this.__intraModel(instance.dnsProvider); + + return await super.get.call(__intraModel, data, ...args); + } + + static listProviders(){ + let out = []; + for(let provider in providers){ + out.push({ + name: provider, + fields: providers[provider]._keyMap, + }); + } + return out; + } + + get api(){ + return new this.constructor.Provider(this); + } + + async listDomains(){ + return this.api.listDomains(); + } + + async updateDomains(domains){ + domains = domains || await this.listDomains(); + let currentDomains = this.domains.map(domain => domain.domain); + + + for(let domain of domains){ + if(currentDomains.includes(domain.domain)){ + delete currentDomains[currentDomains.indexOf(domain.domain)]; + continue; + } + await Domain.create({ + created_by: this.created_by, + domain: domain.domain, + dnsProvider_id: this.id, + zoneId: domain.zoneId, + }); + } + console.log('currentDomains:', currentDomains) + + for(let domain of currentDomains){ + if(!domain) continue + domain = await Domain.get(domain); + await domain.remove(); + } + } + + async remove(){ + for(let domain of await this.domains){ + await domain.remove(); + } + let instance = await super.remove(); + + return instance; + } + + toJSON(){ + return { + ...super.toJSON(), + ...this.constructor.Provider.toJSON() + }; + } +} + +DnsProvider.register(ModelPs(DnsProvider)) + + +if(require.main === module){(async function(){try{ + const conf = require('../conf'); + + // console.log(await DnsProvider.findall()); + + let provider = await DnsProvider.get('e8443e03ac503c7b'); + + console.log(await provider.listDomains()) + + let domain = await Domain.get('holycore.quest') // pork + // let domain = await Domain.get('rm-rf.stream') // DO + // let domain = await Domain.get('test.wtf') // CF + + // console.log(await domain.createRecord({type: 'TXT', name: 'apitewefweefwsewft222', data:'hiiiiiii'})) + + let txtRecords = await domain.getRecords({type: 'TXT'}); + console.log(txtRecords.map(i=>`${i.name}: ${i.data}`)) + // console.log(await domain.deleteRecords({type: 'TXT'})) + + +}catch(error){ + console.log('IIFE Error:', error); +}finally{ + process.exit(0); +}})()} diff --git a/nodejs/models/dns_provider/cloudflare.js b/nodejs/models/dns_provider/cloudflare.js new file mode 100644 index 0000000..c44abe8 --- /dev/null +++ b/nodejs/models/dns_provider/cloudflare.js @@ -0,0 +1,148 @@ +'use strict'; + +const axios = require('axios'); +const {DnsApi} = require('./common'); + +//like the options obj will always use domain data and type +// change content to data +// change name to domain + + +class CloudFlare extends DnsApi{ + static _keyMap = { + token: {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Token'}, + } + + static displayName = 'CloudFlare' + static displayIconHtml = ` + ` + // Cloud icon for cloudflare + static displayIconUni = '' + + constructor(token){ + super() + this.token = token.token || token; + } + + __typeCheck(type){ + if(!type) return; + if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error(`${this.constructor.name} API: Invalid 'type' passed`) + } + + async axios(method, ...args){ + try{ + let a = axios.create({ + baseURL: 'https://api.cloudflare.com/client/v4/zones', + headers: {Authorization: `Bearer ${this.token}`} + }); + + return await a[method](...args); + }catch(error){ + if(!error.response) throw error; + if(error.response.data && error.response.data.errors[0].code == 10000){ + throw this.errors.unauthorized(); + } + throw this.errors.other(error.response.status, error.response.data.errors[0].message, error.response.data.errors[0].code, error); + } + } + + async listDomains(){ + let res = await this.axios('get'); + + for(let domain of res.data.result){ + domain.domain = domain.name + domain.zoneId = domain.id + } + + return res.data.result; + } + + /* + The API and the generic class interface have different opinions of what keys + hold what data, the __parseOptions and __pastseRes normal the keys to what + the class expects + + What the the API calls it : What the class wants it as. + */ + __apiKeyMap = { + 'content': 'data', + } + + //get records + async getRecords(domain, options){ + let res = await this.axios('get', + `${domain.zoneId}/dns_records`, + ); + let records = this.__parseRes(res.data.result); + + if(!options) return records; + + return records.filter((record)=>{ + let matchCount = 0 + for(let key in options){ + if(record[key] === options[key] && ++matchCount === Object.keys(options).length){ + return true; + } + } + }); + } + + async createRecord(domain, options){ + try{ + let res = await this.axios('post', + `${domain.zoneId}/dns_records`, + this.__parseOptions(options, ['type', 'name', 'data']) + ); + + return this.__parseRes([res.data.result])[0]; + }catch(error){ + if(error.APIcode == 81058){ + return (await this.getRecords(domain, options))[0]; + } + throw error; + } + + } + + async deleteRecordById(domain, id){ + let res = await this.axios('delete', `${domain.zoneId}/dns_records/${id}`); + } + + async deleteRecords(domain, options){ + let records = await this.getRecords(domain, options) + for(let record of records){ + + } + + return true; + } +} + +module.exports = CloudFlare; + + +if(require.main === module){(async function(){try{ + // let cf = new CloudFlare(""); + // let domain = { + // domain: "example.uk", + // zoneId: "5eb25c12cd7d22f11252330a29a0dd77" + // } + + // console.log(await cf.listDomains()) + + //content = ip + //name = domain + // console.log('get', await cf.getRecords(domain, {content: '172.206.221.130'})) + + // console.log('post', await cf.createRecord(domain, {name:'test', content: '10.0.0.1', type: "TXT"})) + + // console.log('delete', await cf.deleteRecordById(domain , "5c0e958c3406a34d011459933d538b78")) + + // console.log('delete', await cf.deleteRecords(domain, {type: 'A'})) + +}catch(error){ + console.log('IIFE Error:', error) +}})()} diff --git a/nodejs/models/dns_provider/common.js b/nodejs/models/dns_provider/common.js new file mode 100644 index 0000000..32f88a2 --- /dev/null +++ b/nodejs/models/dns_provider/common.js @@ -0,0 +1,117 @@ +'use strict'; + +const tldExtract = require('tld-extract').parse_host; + +class DnsApi{ + errors = { + unauthorized: ()=>{ + let error = new Error('UnauthorizedDnsApi'); + error.name = 'UnauthorizedDnsApi'; + error.message = `Unauthorized call to ${this.constructor.name}`; + error.status = 424; + + return error; + }, + invalidInput: (keys)=>{ + let error = new Error('InvalidInput'); + error.name = 'InvalidInput'; + error.message = `Required keys missing: ${keys.join(', ')}` + + return error + + }, + other: (status, message, APIcode)=>{ + let error = new Error('OtherDnsApiError'); + error.name = 'OtherDnsApiError'; + error.message = `DNS API Error ${this.constructor.name}: ${status} ${message}`; + error.status = 424; + error.APIcode = APIcode; + return error; + }, + } + + static info(){ + let svgDataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(this.displayIconHtml) + .replace(/'/g, '%27') + .replace(/"/g, '%22')}` + + return { + displayName: this.displayName, + displayIconUni: this.displayIconUni, + displayIconHtml: svgDataUrl, + fields: this._keyMap, + } + } + + static toJSON(){ + return { + ...this.info(), + } + } + + /* + No instance data should ever be shared, so just give the static level inf + */ + toJSON(){ + return this.constructor.toJSON(); + } + + + __typeCheck(type){ + if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error('PorkBun API: Invalid type passed') + } + + + /* + The API and the generic class interface have different opinions of what keys + hold what data, the __parseOptions and __pastseRes normal the keys to what + the class expects + + What the the API calls it : What the class wants it as. + */ + __apiKeyMap = {}; + + __parseOptions(options, keys){ + if(!options && !keys) return undefined; + + if(keys){ + let missingKeys = [] + for(let key of keys){ + if(!options[key]) missingKeys.push(key) + } + + if(missingKeys.length) throw this.errors.invalidInput(missingKeys); + } + + for(let [apiKey, clsKey] of Object.entries(this.__apiKeyMap)){ + if(options[clsKey]){ + options[apiKey] = options[clsKey]; + delete options[clsKey]; + } + } + + if(options.type) this.__typeCheck(options.type); + + return options; + } + + __parseRes(data){ + for(let item of data){ + for(let [apiKey, clsKey] of Object.entries(this.__apiKeyMap)){ + if(item[apiKey]){ + item[clsKey] = item[apiKey]; + } + } + try{ + item.name = tldExtract(item.name).sub + }catch{} + } + + return data; + } +} + +module.exports = { + DnsApi, +}; + diff --git a/nodejs/models/dns_provider/digitalocean.js b/nodejs/models/dns_provider/digitalocean.js new file mode 100644 index 0000000..264c827 --- /dev/null +++ b/nodejs/models/dns_provider/digitalocean.js @@ -0,0 +1,85 @@ +'use strict'; + +const axios = require('axios'); +const {DnsApi} = require('./common'); + +class DigitalOcean extends DnsApi{ + static _keyMap = { + token: {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Token'}, + } + + static displayName = 'DigitalOcean' + static displayIconHtml = ` +` + // '' + static displayIconUni = '' + + constructor(token){ + super() + this.token = token.token || token; + } + + async axios(method, ...args){ + try{ + let a = axios.create({ + baseURL: 'https://api.digitalocean.com/v2/', + headers: {Authorization: `Bearer ${this.token}`} + }); + + return await a[method](...args); + }catch(error){ + if(!error.response) throw error; + if(error.response.data && error.response.data.id === 'Unauthorized'){ + throw this.errors.unauthorized(); + } + throw this.errors.other(error.response.status, error.response.data.message); + } + } + + async listDomains(){ + let res = await this.axios('get', '/domains'); + + return this.__parseRes(res.data.domains); + } + + async getRecords(domain, options){ + options = this.__parseOptions(options); + let res = await this.axios('get', `/domains/${domain.domain}/records`, {params: options}) + let records = this.__parseRes(res.data.domain_records); + if(!options) return records; + + return records.filter((record)=>{ + let matchCount = 0 + for(let key in options){ + if(record[key] === options[key] && ++matchCount === Object.keys(options).length){ + return true; + } + } + }); + } + + async createRecord(domain, options){ + options = this.__parseOptions(options, ['type', 'name', 'data']); + let res = await this.axios('post', `/domains/${domain.domain}/records`, options); + + return this.__parseRes([res.data.domain_record])[0]; + } + + async deleteRecordById(domain, id){ + let res = await this.axios('delete', `/domains/${domain.domain}/records/${id}`); + } + + async deleteRecords(domain, options){ + let records = await this.getRecords(domain, options) + for(let record of records){ + let res = await this.deleteRecordById(domain, record.id); + } + + return true; + } +} + +module.exports = DigitalOcean; diff --git a/nodejs/models/dns_provider/porkbun.js b/nodejs/models/dns_provider/porkbun.js new file mode 100644 index 0000000..64347d8 --- /dev/null +++ b/nodejs/models/dns_provider/porkbun.js @@ -0,0 +1,133 @@ +'use strict'; + +const axios = require('axios'); +const {DnsApi} = require('./common'); + + +class PorkBun extends DnsApi{ + static _keyMap = { + 'apiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API key'}, + 'secretApiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Secret key'}, + } + + static displayName = 'PorkBun'; + static displayIconUni = ''; + static displayIconHtml = ` +` + + constructor(args){ + super() + this.apiKey = args.apiKey; + this.secretApiKey = args.secretApiKey; + } + + async post(url, data){ + let res; + try{ + data = { + ...(data || {}), + secretapikey: this.secretApiKey, + apikey: this.apiKey, + }; + res = await axios.post(`https://api.porkbun.com/api/json/v3${url}`, data); + + return res; + }catch(error){ + if(!error.response) throw error; + if(error.response.data.message.includes('Invalid API key')){ + throw this.errors.unauthorized(); + } + // console.error('API error:', error) + throw this.errors.other(error.response.status, error.response.data.message) + } + } + + __typeCheck(type){ + if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error('PorkBun API: Invalid type passed') + } + + __parseName(domain, name){ + if(name && !name.endsWith('.'+domain)){ + return `${name}.${domain}` + } + return name; + } + + + /* + The API and the generic class interface have different opinions of what keys + hold what data, the __parseOptions and __pastseRes normal the keys to what + the class expects + + What the the API calls it : What the class wants it as. + */ + + __apiKeyMap = { + 'content': 'data' + } + + async getRecords(domain, options){ + let res = await this.post(`/dns/retrieve/${domain}`); + let records = this.__parseRes(res.data.records) + if(!options) return records; + options = this.__parseOptions(options); + + return records.filter((record)=>{ + let matchCount = 0 + for(let key in options){ + if(record[key] === options[key] && ++matchCount === Object.keys(options).length){ + return true; + } + } + }); + } + + async createRecord(domain, options, force){ + if(force){ + await this.deleteRecords(domain, options) + } + + try{ + options = this.__parseOptions(options, ['type', 'name', 'data']); + let res = await this.post(`/dns/create/${domain}`, options); + + return res.data.result; + }catch(error){ + if(error.message && error.message.includes('We were unable to create the DNS record')){ + return (await this.getRecords(domain, options))[0]; + } + } + } + + async deleteRecordById(domain, id){ + let res = await this.post(`/dns/delete/${domain.domain}/${id}`); + return res.data; + } + + + async deleteRecords(domain, options){ + let records = await this.getRecords(domain, options); + for(let record of records){ + await this.deleteRecordById(domain, record.id) + } + } + + + async listDomains(){ + let res = await this.post(`/domain/listAll`, {"includeLabels": "yes"}); + return res.data.domains; + } +} + +module.exports = PorkBun; diff --git a/nodejs/models/host.js b/nodejs/models/host.js index 0453899..ca04014 100755 --- a/nodejs/models/host.js +++ b/nodejs/models/host.js @@ -1,19 +1,19 @@ 'use strict'; -const Table = require('../utils/redis_model'); +const Table = require('.'); +const {Domain} = require('.').models; const ModelPs = require('../utils/model_pubsub'); const tldExtract = require('tld-extract').parse_host; -const PorkBun = require('../utils/porkbun'); const LetsEncrypt = require('../utils/letsencrypt'); const conf = require('../conf'); -let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); -let letsEncrypt = new LetsEncrypt({ - directoryUrl: LetsEncrypt.AcmeClient.directory.letsencrypt.staging, +const letsEncrypt = new LetsEncrypt({ + directoryUrl: conf.environment === "production" ? + LetsEncrypt.AcmeClient.directory.letsencrypt.production : + LetsEncrypt.AcmeClient.directory.letsencrypt.staging, }); - class Host extends Table{ static _key = 'host'; static _keyMap = { @@ -28,9 +28,10 @@ class Host extends Table{ 'targetssl': {isRequired: false, default: false, type: 'boolean'}, 'is_cache': {default: false, isRequired: false, type: 'boolean',}, 'is_wildcard': {default: false, isRequired: false, type: 'boolean',}, - 'wildcard_status': {isRequired: false, type: 'string', min: 3, max: 500, default: 'Requesting'}, + 'wildcard_status': {isRequired: false, type: 'string', min: 3, max: 500}, 'wildcard_parent': {isRequired: false, type: 'string', min: 3, max: 500}, 'wildcard_expires': {isRequired: false, type: 'number'}, + 'domain': {model: 'Domain', rel: 'one'}, } static lookUpObj = {}; @@ -38,7 +39,6 @@ class Host extends Table{ static async addCache(host, parentOBJ){ try{ - console.log('addCache host:', host, 'parentOBJ host', parentOBJ.host) parentOBJ = await this.get(parentOBJ.host); @@ -88,6 +88,8 @@ class Host extends Table{ static async create(data, ...args){ try{ + if(data.is_wildcard) await this.validateWildcardCreate(data, args); + let out = await super.create(data, ...args); await this.buildLookUpObj(); if(out.is_wildcard) out.createWildcardCert(); @@ -99,6 +101,17 @@ class Host extends Table{ } } + static async validateWildcardCreate(data, ...args){ + try{ + if(!data.host.startsWith('*.')) throw new Error('not wild card'); + await Domain.get(data.host); + }catch(error){ + console.log('validateWildcardCreate error', error) + if(error.status === 404) error.message = "No matching DNS provider registered" + throw this.errors.ObjectValidateError([{key: 'host', message: error.message}]); + } + } + async createWildcardCert(){ if(!this.host.startsWith('*.')) throw new Error('not wild card'); @@ -116,12 +129,11 @@ class Host extends Table{ try{ let parts = tldExtract(authz.identifier.value); - let res = await porkBun.createRecordForce( - parts.domain, + let res = await host.domain.createRecord( { type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, - content: `${keyAuthorization}` + data: `${keyAuthorization}` } ); }catch(error){ @@ -162,8 +174,7 @@ class Host extends Table{ }) try{ let parts = tldExtract(authz.identifier.value); - await porkBun.deleteRecords( - parts.domain, + await host.domain.deleteRecords( { type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : '' @@ -322,6 +333,7 @@ class Host extends Table{ return true; } } +Host.register(ModelPs(Host)) class Cached extends Table{ @@ -338,9 +350,16 @@ class Cached extends Table{ module.exports = {Host: ModelPs(Host)}; -(async function(){ +if(require.main === module){(async function(){ try{ await Host.lookUpReady(); + + let host = await Host.get('*.new.test.wtf') + + console.log('host', host.domain.provider.api); + + + // let res = await Host.create({ // host: '*.test.holycore.quest', // ip: '192.168.1.47', @@ -353,8 +372,8 @@ try{ // console.log('IIFE res:\n', res) // console.log(Host.test(55)) - console.log(await Host.list()) - console.log(await Cached.listDetail()) + // console.log(await Host.list()) + // console.log(await Cached.listDetail()) // console.log('IIFE lookup:', Host.lookUp('bld3324sdf.test.holycore.quest')) @@ -389,4 +408,4 @@ try{ }catch(error){ console.log('IIFE test area error:', error) } -})() \ No newline at end of file +})()} diff --git a/nodejs/models/index.js b/nodejs/models/index.js new file mode 100644 index 0000000..36b62cc --- /dev/null +++ b/nodejs/models/index.js @@ -0,0 +1,9 @@ +'use strict'; + +const Table = require('../utils/redis_model') +module.exports = Table; + +require('./dns_provider'); +require('./host'); +require('./token'); +require('./user'); diff --git a/nodejs/models/token.js b/nodejs/models/token.js index ee566a2..d108c1a 100644 --- a/nodejs/models/token.js +++ b/nodejs/models/token.js @@ -1,7 +1,6 @@ 'use strict'; -const Table = require('../utils/redis_model'); -const {User} = require('./user'); +const Table = require('.'); const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}; @@ -11,8 +10,8 @@ class Token extends Table{ 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, 'created_on': {default: function(){return (new Date).getTime()}}, 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, - 'token': {default: UUID, type: 'string', min: 36, max: 36}, - 'is_valid': {default: true, type: 'boolean'} + 'token': {default: UUID, type: 'string', min: 36, max: 36, isPrivate: true}, + 'is_valid': {default: true, type: 'boolean'}, } constructor(...args){ @@ -28,13 +27,12 @@ class Token extends Table{ } } -class AuthToken extends Token{ - constructor(...args){ - super(...args); - } +Token.register(); - async getUser(){ - return await User.get(this.created_by); +class AuthToken extends Token{ + static _keyMap = { + ...super._keyMap, + user: {model: 'User', rel: 'one', localKey: 'created_by'}, } static async create(data){ @@ -42,8 +40,8 @@ class AuthToken extends Token{ return super.create(data) } - } +AuthToken.register(); class InviteToken extends Token{ static _keyMap = { @@ -66,5 +64,6 @@ class InviteToken extends Token{ } } } +InviteToken.register(); module.exports = {Token, InviteToken, AuthToken}; diff --git a/nodejs/models/user_redis.js b/nodejs/models/user_redis.js index 3993eff..e94a1d0 100644 --- a/nodejs/models/user_redis.js +++ b/nodejs/models/user_redis.js @@ -12,7 +12,7 @@ class User extends Table{ 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, 'username': {isRequired: true, type: 'string', min: 3, max: 500}, - 'password': {isRequired: true, type: 'string', min: 3, max: 500}, + 'password': {isRequired: true, type: 'string', min: 3, max: 500, isPrivate: true}, 'backing': {default:"redis", isRequired: false, type: 'string',}, } @@ -58,12 +58,9 @@ class User extends Table{ throw error; } }; - - } -module.exports = {User}; - +User.register(); (async function(){ var defaultUser = 'proxyadmin2' diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 1c6c0ed..75092d5 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^6.4.2", + "@popperjs/core": "^2.11.8", "acme-client": "^5.4.0", "axios": "^1.7.2", "bcrypt": "^5.1.1", @@ -197,7 +198,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" diff --git a/nodejs/package.json b/nodejs/package.json index 709de97..24e6fb0 100755 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^6.4.2", + "@popperjs/core": "^2.11.8", "acme-client": "^5.4.0", "axios": "^1.7.2", "bcrypt": "^5.1.1", diff --git a/nodejs/public/css/styles.css b/nodejs/public/css/styles.css index 382cb50..7db0c02 100755 --- a/nodejs/public/css/styles.css +++ b/nodejs/public/css/styles.css @@ -1,19 +1,14 @@ -body { - height: 100vh; -} - nav.navbar{ padding-left: 1em; padding-right: 1em; } -div.card-body{ - padding-left: 1.5em; - padding-right: 1.5em; +#spa-shell { + margin-top: 4.5rem; + padding-bottom: 1em; } -#spa-shell { - padding-top: 4.5rem; - position: absolute; - height: 100%; +.card-title{ + font-weight: bold; } + diff --git a/nodejs/public/lib/js/app-base.js b/nodejs/public/lib/js/app-base.js index 8b785aa..486cb0c 100644 --- a/nodejs/public/lib/js/app-base.js +++ b/nodejs/public/lib/js/app-base.js @@ -76,7 +76,8 @@ app.api = (function(app){ var baseURL = '/api/' function post(url, data, callback){ - $.ajax({ + if(!$.isFunction(callback)) callback = callback2; + return $.ajax({ type: 'POST', url: baseURL+url, headers:{ @@ -86,17 +87,18 @@ app.api = (function(app){ contentType: "application/json; charset=utf-8", dataType: "json", complete: function(res, text){ - callback( + callback ? callback( text !== 'success' ? res.statusText : null, JSON.parse(res.responseText), res.status - ) + ) : function(){} } }); } function put(url, data, callback){ - $.ajax({ + if(!$.isFunction(callback)) callback = callback2; + return $.ajax({ type: 'PUT', url: baseURL+url, headers:{ @@ -106,18 +108,18 @@ app.api = (function(app){ contentType: "application/json; charset=utf-8", dataType: "json", complete: function(res, text){ - callback( + callback ? callback( text !== 'success' ? res.statusText : null, JSON.parse(res.responseText), res.status - ) + ) : function(){} } }); } function remove(url, callback, callback2){ if(!$.isFunction(callback)) callback = callback2; - $.ajax({ + return $.ajax({ type: 'delete', url: baseURL+url, headers:{ @@ -126,17 +128,36 @@ app.api = (function(app){ contentType: "application/json; charset=utf-8", dataType: "json", complete: function(res, text){ - callback( + callback ? callback( text !== 'success' ? res.statusText : null, JSON.parse(res.responseText), res.status - ) + ) : function(){} + } + }); + } + + function options(url, callback){ + return $.ajax({ + type: 'OPTIONS', + url: baseURL+url, + headers:{ + 'auth-token': app.auth.getToken() + }, + contentType: "application/json; charset=utf-8", + dataType: "json", + complete: function(res, text){ + callback ? callback( + text !== 'success' ? res.statusText : null, + JSON.parse(res.responseText), + res.status + ) : function(){} } }); } function get(url, callback){ - $.ajax({ + return $.ajax({ type: 'GET', url: baseURL+url, headers:{ @@ -145,16 +166,16 @@ app.api = (function(app){ contentType: "application/json; charset=utf-8", dataType: "json", complete: function(res, text){ - callback( + callback ? callback( text !== 'success' ? res.statusText : null, JSON.parse(res.responseText), res.status - ) + ) : function(){} } }); } - return {post: post, get: get, put: put, delete: remove} + return {post: post, get: get, put: put, delete: remove, options: options,} })(app) app.auth = (function(app){ @@ -342,7 +363,12 @@ $( document ).ready(function(){ }); $('.fa-circle-minus').click(function(){ - $(this).closest('.card').find('.card-body').slideToggle('fast'); + let $body = $(this).closest('.card').find('.card-body'); + if($body.hasClass('d-none')){ + $body.removeClass("d-none").removeClass('d-md-block'); + if($body.is(":visible")) $body.hide(); + } + $body.slideToggle('fast'); }); $('.fa-circle-xmark').click(function(){ @@ -363,6 +389,17 @@ $( document ).ready(function(){ }, 30000,); }); +(function($){ + $.fn.scrollTo = function(){ + const yOffset = Number($('#spa-shell').css('margin-top').replace('px', '')); + const y = this[0].getBoundingClientRect().top + window.scrollY - yOffset; + + console.log('y', y) + window.scrollTo({top: y, behavior: 'smooth'}); + }; + +})(jQuery); + //ajax form submit function formAJAX(btn){ event.preventDefault(btn); // avoid to execute the actual submit of the form. diff --git a/nodejs/public/lib/js/jq-repeat_new.js b/nodejs/public/lib/js/jq-repeat_new.js index fb8e16e..c4da556 100644 --- a/nodejs/public/lib/js/jq-repeat_new.js +++ b/nodejs/public/lib/js/jq-repeat_new.js @@ -6,11 +6,22 @@ MIT license (function($, Mustache){ 'use strict'; - if (!$.scope) { - $.scope = {}; - } - - var make = function(element){ + var scope = {}; + + $.scope = new Proxy(scope, { + get(obj, prop){ + if(!obj[prop]){ + scope[prop] = []; + } + return Reflect.get(...arguments); + }, + set(obj, prop, value) { + + return Reflect.set(...arguments); + }, + }); + + var make = function(element, template){ var result = []; result.splice = function(inputValue, ...args){ @@ -82,7 +93,7 @@ MIT license //figure out new elements index var key = I + index; // apply values to template - var render = Mustache.render(this.__jqTemplate, toAdd[I]); + var render = Mustache.render(this.__jqTemplate, this.__buildData(i, toAdd[I])); //set call name and index keys to DOM element var $render = $( render ).addClass( 'jq-repeat-'+ this.__jqRepeatId ).attr( 'jq-repeat-index', key ); @@ -217,10 +228,10 @@ MIT license } this[index] = $.extend( true, this[index], data ); - var $render = $(Mustache.render(this.__jqTemplate, this[index])); + var $render = $(Mustache.render(this.__jqTemplate, this.__buildData(index, this[index]))); $render.attr('jq-repeat-index', index); - this.__update(this[index].__jq_$el, $render, this[index], this); + this.__putUpdate(this[index].__jq_$el, $render, this[index], this); this[index].__jq_$el = $render; }; @@ -228,6 +239,8 @@ MIT license return this[this.indexOf(key, value)]; } + // User definable helper methods + result.__put = function($el, item, list){ $el.show(); }; @@ -236,42 +249,76 @@ MIT license $el.remove(); }; - result.__update = function($el, $render, item, list){ + result.__putUpdate = function($el, $render, item, list){ $el.replaceWith($render); $el.show(); }; - result.__setPut = function(fn) { - Object.defineProperty(this, '__put', { - value: fn, - writable: true, - enumerable: false, - configurable: true - }); + result.__parseData = function(data){ + return data; + } + + // internal helper methods + + result.__buildData = function(index, data){ + return { + ...this.__parseData(data), + nestedTemplates: this.__parseNestedTemplates(index, data), + _parent: this._parentData, + }; }; - result.__setTake = function(fn) { - Object.defineProperty(this, '__take', { - value: fn, - writable: true, - enumerable: false, - configurable: true - }); - }; + result.__parseNestedTemplates = function(index, data){ + let templates = [] + let tempData = { + ...data, + _parent: data, + }; - result.__setUpdate = function(fn) { - Object.defineProperty(this, '__update', { - value: fn, - writable: true, - enumerable: false, - configurable: true - }); - }; + for(let idx in this.nestedTemplates){ + let $el = $(`${this.nestedTemplates[idx]}`); - var $this = $( element ); + $el.attr('jq-repeat', Mustache.render($el.attr('jq-repeat'), tempData)); + $el.attr('jq-repeat-index', Mustache.render($el.attr('jq-repeat-index'), tempData)); + $el.attr('jq-repeat-parent', this.__jqRepeatId); + $el.attr('jq-repeat-parent-index', index); + templates[idx] = $el[0].outerHTML; + } + + return templates; + } + + for(let prop of ['put', 'take', 'putUpdate', 'parseData']){ + Object.defineProperty(result, prop, { + enumerable: false, + get(){ + return this[`__${prop}`] + }, + set(value) { + this[`__${prop}`] = value; + }, + }); + } + + var $this = $(element); + result.nestedTemplates = []; result.__jqRepeatId = $this.attr( 'jq-repeat' ); $this.removeAttr('jq-repeat'); result.__index = $this.attr('jq-repeat-index'); + + if($this.attr('jq-repeat-parent')){ + result._parentData = $.scope[$this.attr('jq-repeat-parent')][$this.attr('jq-repeat-parent-index')] + result.__jqParent = $this.attr('jq-repeat-parent'); + result.__jqParentIndex = $this.attr('jq-repeat-parent-index'); + } + + $this.find('[jq-repeat]').each((idx, el)=>{ + let templateIdx = result.nestedTemplates.length; + let template = `${el.outerHTML}`; + result.nestedTemplates.push(template); + $(el).replaceWith(`{{{ nestedTemplates.${templateIdx} }}}`); + }); + result.__jqTemplate = $this[0].outerHTML; $this.replaceWith( ' + + + + +
+<%- include('bottom') %> diff --git a/nodejs/views/hosts.ejs b/nodejs/views/hosts.ejs index 255aa10..5652fc0 100755 --- a/nodejs/views/hosts.ejs +++ b/nodejs/views/hosts.ejs @@ -10,13 +10,6 @@ margin-bottom: 1px; } - .card-title{ - font-weight: bold; - } - - div.hostEditPanel{ - margin-bottom: 1em; - } div.form-group{ margin-bottom: 1em; } @@ -62,19 +55,19 @@ } function hostPopulate(){ - app.host.list(function(error, res){ + app.api.get('host?detail=1&provider=1', function(error, res){ if(error) return app.util.actionMessage(error, $.scope.hosts.$this, 'danger'); - for(let host of res){ + for(let host of res.results){ $.scope.hosts.push(hostParseRow(host)); } - $.scope.hosts.__setPut(function($el, item, list){ + $.scope.hosts.put = function($el, item, list){ $el.addClass('table-success'); $el.fadeIn(2000, function(){ $el.removeClass('table-success'); }); - }); + }; }); } @@ -89,14 +82,21 @@ hostEditCancle(); host = $.scope.hosts.getByKey(host); host.__jq_$el.addClass('table-warning'); + $editHostForm.find('[name=is_wildcard').attr('disabled', true); $.scope.editHost.update({...host, form: $editHostForm.html()}); + if(host.is_wildcard){ + $('.hostEditPanel [name="host"]').attr('disabled', true); + } + $.each(host, function( key, value ) { if(typeof value == "boolean"){ $(".hostEditPanel #"+ key +"-"+ value).prop('checked', true) }else{ $(".hostEditPanel input[name='" + key + "']").val(value); } }); + + $('.hostEditPanel').scrollTo(); }; function hostDownloadCert(host, type){ @@ -127,32 +127,31 @@ $editHostForm = $('#addHost').clone(); $editHostForm.find('hr.buttonBreak').nextAll().remove(); // $editHostForm.find('.autoSll').addClass('bg-secondary'); - $editHostForm.find('[name=is_wildcard').attr('disabled', true); hostPopulate(); //populate the table - $.scope.hosts.__setTake(function($el, item, list){ + $.scope.hosts.take = function($el, item, list){ $el.addClass('table-danger'); $el.fadeOut(1000, function(){ $el.remove() }); - }); + }; - $.scope.hosts.__setUpdate(function($el, $render, item, list){ + $.scope.hosts.putUpdate = function($el, $render, item, list){ $render.show() $el.replaceWith($render); - }); + }; - $.scope.editHost.__setPut(function($el, item, list){ + $.scope.editHost.put = function($el, item, list){ $el.slideDown(); - }); + }; - $.scope.editHost.__setTake(function($el, item, list){ + $.scope.editHost.take = function($el, item, list){ $el.slideUp(); - }); + }; - // app.subscribe(/^model:Host/, function(data, topic){ - // console.log(topic, data); - // }); + app.subscribe(/^model:Host/, function(data, topic){ + console.log(topic, data); + }); app.subscribe(/^model:Host:create/, function(data, topic){ let [a,b, action, host] = topic.split(':'); @@ -187,7 +186,7 @@ - -