From 3df3341235e63b4c6ba6faa58494ed836b2c4584 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sat, 10 Aug 2024 17:47:44 -0400 Subject: [PATCH 01/21] DNS stuff --- nodejs/models/dnsProvider.js | 154 +++++++++++++ .../{utils => models/dns_provider}/porkbun.js | 27 ++- nodejs/models/host.js | 6 +- nodejs/models/user_redis.js | 2 +- nodejs/public/lib/js/app-base.js | 21 +- nodejs/public/lib/js/val.js | 3 + nodejs/routes/api.js | 2 + nodejs/routes/dns.js | 145 ++++++++++++ nodejs/routes/render.js | 5 + nodejs/utils/redis_model.js | 12 + nodejs/views/dns.ejs | 218 ++++++++++++++++++ nodejs/views/top.ejs | 3 + 12 files changed, 587 insertions(+), 11 deletions(-) create mode 100644 nodejs/models/dnsProvider.js rename nodejs/{utils => models/dns_provider}/porkbun.js (81%) create mode 100644 nodejs/routes/dns.js create mode 100644 nodejs/views/dns.ejs diff --git a/nodejs/models/dnsProvider.js b/nodejs/models/dnsProvider.js new file mode 100644 index 0000000..1b7f810 --- /dev/null +++ b/nodejs/models/dnsProvider.js @@ -0,0 +1,154 @@ +'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 = { + PorkBun: require('./dns_provider/porkbun'), + DigitalOcean: require('./dns_provider/digitalocean'), +}; + +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'}, + } + + static async get(data, ...args){ + let instance = await super.get(data, ...args); + // instance.provider = (await DnsProvider.get(instance.dnsProvider_id)).provider; + + return instance; + } + + static async getByProviderId(id){ + let domains = await this.listDetail(); + let results = []; + for(let domain of domains){ + if(domain.dnsProvider_id == id) results.push(domain); + } + + return results; + } +} + +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'}, + } + + 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 listProviders(){ + let out = []; + for(let provider in providers){ + out.push({ + name: provider, + displayName: providers[provider].displayName || provider, + displayIconUni: providers[provider].displayIconUni || '?', + displayIconHtml: providers[provider].displayIconHtml || '', + fields: providers[provider]._keyMap, + }); + } + return out; + } + + static async create(data, ...args){ + let __intraModel = this.__intraModel(data.dnsProvider) + + let instance = await super.create.call(__intraModel, data); + if(!data.noUpdate) instance.updateDomains(); + return instance; + } + + static async get(data, ...args){ + let instance = await super.get(data); + let __intraModel = this.__intraModel(instance.dnsProvider) + + instance = await super.get.call(__intraModel, data); + instance.provider = new __intraModel.Provider(instance); + // instance.domains = Domain.getByProviderId(this.id); + + return instance; + } + + async updateDomains(){ + try{ + let domains = await this.provider.listDomains(); + console.log('got domains', domains) + for(let domain of domains){ + if(!(await Domain.exists(domain.domain))){ + await Domain.create({ + created_by: this.created_by, + domain: domain.domain, + dnsProvider_id: this.id + }); + } + } + }catch(error){ + console.error('updateDomains error', error) + } + } + + async getDomains(){ + return Domains.getByProviderId(this.id) + } + + async remove(){ + let id = this.id + let instance = await super.remove() + // get all domains for this provider and delete them + + return instance + } + + toJSON(){ + return { + ...super.toJSON(), + domains: this.domains, + } + } +} + +Domain = ModelPs(Domain); +DnsProvider = ModelPs(DnsProvider); + +module.exports = {Domain, DnsProvider} + +if(require.main === module){(async function(){try{ + const conf = require('../conf'); + + + // console.log(aw) + +}catch(error){ + console.log('IIFE Error:', error) +}})()} diff --git a/nodejs/utils/porkbun.js b/nodejs/models/dns_provider/porkbun.js similarity index 81% rename from nodejs/utils/porkbun.js rename to nodejs/models/dns_provider/porkbun.js index 897b459..23c3d8f 100644 --- a/nodejs/utils/porkbun.js +++ b/nodejs/models/dns_provider/porkbun.js @@ -2,12 +2,18 @@ const axios = require('axios'); + class PorkBun{ + static _keyMap = { + 'apiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API key'}, + 'secretApiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Secret key'}, + } + baseUrl = 'https://api.porkbun.com/api/json/v3'; - constructor(apiKey, secretApiKey){ - this.apiKey = apiKey; - this.secretApiKey = secretApiKey; + constructor(args){ + this.apiKey = args.apiKey; + this.secretApiKey = args.secretApiKey; } async post(url, data){ @@ -40,6 +46,7 @@ class PorkBun{ async getRecords(domain, options){ let res = await this.post(`/dns/retrieve/${domain}`); if(!options) return res.data.records; + if(options.data) options.content = options.data; if(options.type) this.__typeCheck(options.type); if(options.name) options.name = this.__parseName(domain, options.name); @@ -50,8 +57,8 @@ class PorkBun{ for(let option in options){ if(record[option] === options[option] && ++matchCount === Object.keys(options).length){ records.push(record) + break; } - // console.log('option', option, options[option], record[option], matchCount) } } @@ -59,7 +66,7 @@ class PorkBun{ } async deleteRecordById(domain, id){ - let res = this.post(`/dns/delete/${domain}/${id}`); + let res = await this.post(`/dns/delete/${domain}/${id}`); return res.data; } @@ -77,7 +84,7 @@ class PorkBun{ // if(options.name) options.name = this.__parseName(domain, options.name); // console.log('PorkBun.createRecord to send:', domain, options) - let res = this.post(`/dns/create/${domain}`, options); + let res = await this.post(`/dns/create/${domain}`, options); return res.data; } @@ -93,6 +100,11 @@ class PorkBun{ } return await this.createRecord(domain, options) } + + async listDomains(){ + let res = await this.post(`/domain/listAll`, {"includeLabels": "yes"}); + return res.data.domains; + } } module.exports = PorkBun; @@ -100,8 +112,11 @@ module.exports = PorkBun; if(require.main === module){(async function(){try{ const conf = require('../conf'); + // let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); + // console.log(await porkBun.listDomains()) + // console.log(await porkBun.deleteRecordById('holycore.quest', '415509355')) // console.log('IIFE', await porkBun.createRecordForce('holycore.quest', {type:'A', name: 'testapi', content: '127.0.0.5'})) // console.log('IIFE', await porkBun.getRecords('holycore.quest', {type:'A', name: 'testapi'})) diff --git a/nodejs/models/host.js b/nodejs/models/host.js index 0453899..7e6bd4f 100755 --- a/nodejs/models/host.js +++ b/nodejs/models/host.js @@ -4,7 +4,7 @@ const Table = require('../utils/redis_model'); const ModelPs = require('../utils/model_pubsub'); const tldExtract = require('tld-extract').parse_host; -const PorkBun = require('../utils/porkbun'); +const PorkBun = require('./dns_provider/porkbun'); const LetsEncrypt = require('../utils/letsencrypt'); const conf = require('../conf'); @@ -353,8 +353,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')) diff --git a/nodejs/models/user_redis.js b/nodejs/models/user_redis.js index 3993eff..a096ded 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',}, } diff --git a/nodejs/public/lib/js/app-base.js b/nodejs/public/lib/js/app-base.js index 8b785aa..e8a4fbe 100644 --- a/nodejs/public/lib/js/app-base.js +++ b/nodejs/public/lib/js/app-base.js @@ -135,6 +135,25 @@ app.api = (function(app){ }); } + function options(url, callback){ + $.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( + text !== 'success' ? res.statusText : null, + JSON.parse(res.responseText), + res.status + ) + } + }); + } + function get(url, callback){ $.ajax({ type: 'GET', @@ -154,7 +173,7 @@ app.api = (function(app){ }); } - 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){ diff --git a/nodejs/public/lib/js/val.js b/nodejs/public/lib/js/val.js index 1c340ee..b372555 100755 --- a/nodejs/public/lib/js/val.js +++ b/nodejs/public/lib/js/val.js @@ -50,6 +50,9 @@ var value = this.val(); //link to input value var message; + if(this.prop('disabled')) return true; + + //checks if field is required, and length if(!isNaN(options) && value.length < options){ message = `Must be ${options} characters`; diff --git a/nodejs/routes/api.js b/nodejs/routes/api.js index 5f51432..6e97c92 100644 --- a/nodejs/routes/api.js +++ b/nodejs/routes/api.js @@ -13,6 +13,8 @@ router.use('/user', middleware.auth, require('./user')); // API routes for working with hosts. All endpoints need to be have valid user. router.use('/host', middleware.auth, require('./host')); +router.use('/dns', middleware.auth, require('./dns')); + // API routes for working with hosts. All endpoints need to be have valid user. router.use('/cert', middleware.auth, require('./cert')); diff --git a/nodejs/routes/dns.js b/nodejs/routes/dns.js new file mode 100644 index 0000000..3387968 --- /dev/null +++ b/nodejs/routes/dns.js @@ -0,0 +1,145 @@ +'use strict'; + +const router = require('express').Router(); +const {DnsProvider, Domain} = require('../models/dnsProvider'); + +const Model = DnsProvider; + +router.get('/', async function(req, res, next){ + try{ + return res.json({ + results: await Model[req.query.detail ? "listDetail" : "list"]() + }); + }catch(error){ + return next(error); + } +}); + +router.options('/', async function(req, res, next){ + try{ + return res.json({ + results: await Model.listProviders() + }); + }catch(error){ + return next(error); + } +}); + +router.post('/', async function(req, res, next){ + try{ + req.body.created_by = req.user.username; + let item = await Model.create(req.body); + + return res.json({ + message: `"${item[Model._key]}" added.`, + ...item, + }); + } catch (error){ + next(error); + } +}); + +router.get('/domain', async function(req, res, next){ + try{ + return res.json({ + results: await Domain[req.query.detail ? "listDetail" : "list"]() + }); + }catch(error){ + return next(error); + } +}); + +router.get('/domain/byProvider/:item', async function(req, res, next){ + try{ + console.log('byProvider', req.params.item, await Domain.getByProviderId(req.params.item)) + return res.json({ + results: await Domain.getByProviderId(req.params.item) + }); + }catch(error){ + return next(error); + } +}); + +router.post('/domain/refresh/:item', async function(req, res, next){ + try{ + let item = await Model.get(req.params.item); + item.updateDomains(); + return res.json({}); + }catch(error){ + next(error); + } +}) + + + +router.get('/lookup/:item', async function(req, res, next){ + try{ + return res.json({ + string: req.params.item, + results: await Model.lookUp(req.params.item), + }); + + }catch(error){ + return next(error); + } +}); + +router.get('/:item', async function(req, res, next){ + try{ + + return res.json({ + item: req.params.item, + results: await Model.get(req.params.item) + }); + }catch(error){ + return next(error); + } +}); + +router.put('/:item', async function(req, res, next){ + try{ + req.body.updated_by = req.user.username; + let item = await Model.get(req.params.item); + item = await item.update(req.body); + + return res.json({ + message: `"${req.params.item}" updated.`, + __requestedHost: req.params.item, + ...item, + }); + + }catch(error){ + return next(error); + + } +}); + +router.delete('/:item', async function(req, res, next){ + try{ + let item = await Model.get(req.params.item); + let count = await item.remove(); + + return res.json({ + message: `${req.params.item} deleted`, + ...item, + }); + + }catch(error){ + return next(error); + } +}); + +router.put('/:item/renew', async function(req, res, next){ + try{ + let item = await Model.get(req.params.item); + item.createWildcardCert(); + + return res.json({ + message: `Requesting wildcard cert for ${req.params.item}`, + }) + }catch(error){ + next(error); + } +}); + +module.exports = router; diff --git a/nodejs/routes/render.js b/nodejs/routes/render.js index f2995e5..8366ff4 100644 --- a/nodejs/routes/render.js +++ b/nodejs/routes/render.js @@ -32,6 +32,11 @@ router.get('/hosts', async function(req, res, next) { res.render('hosts', {...values}); }); +router.get('/dns', async function(req, res, next) { + res.render('dns', {...values}); +}); + + router.get('/users', async function(req, res, next) { res.render('users', {...values}); }); diff --git a/nodejs/utils/redis_model.js b/nodejs/utils/redis_model.js index a99cb85..3dc7af5 100644 --- a/nodejs/utils/redis_model.js +++ b/nodejs/utils/redis_model.js @@ -22,6 +22,7 @@ class Table{ static async get(index){ try{ + if(typeof index === 'object'){ index = index[this._key]; } @@ -210,6 +211,17 @@ class Table{ } }; + toJSON(){ + let result = {}; + for (const [key, keyProps] of Object.entries(this.constructor._keyMap)) { + if(!keyProps.isPrivate) result[key] = this[key]; + } + + return result + + // return JSON.stringify(result); + } + } diff --git a/nodejs/views/dns.ejs b/nodejs/views/dns.ejs new file mode 100644 index 0000000..4918deb --- /dev/null +++ b/nodejs/views/dns.ejs @@ -0,0 +1,218 @@ +<%- include('top') %> + + + + + + +<%- include('bottom') %> diff --git a/nodejs/views/top.ejs b/nodejs/views/top.ejs index 404b824..82fcba8 100755 --- a/nodejs/views/top.ejs +++ b/nodejs/views/top.ejs @@ -41,6 +41,9 @@ + From 3e989992df4e918f2d7ecbcb4ff3994a9895b9d3 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sat, 10 Aug 2024 23:09:55 -0400 Subject: [PATCH 02/21] Added digital ocean --- nodejs/models/dns_provider/digitalocean.js | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 nodejs/models/dns_provider/digitalocean.js diff --git a/nodejs/models/dns_provider/digitalocean.js b/nodejs/models/dns_provider/digitalocean.js new file mode 100644 index 0000000..8ff5306 --- /dev/null +++ b/nodejs/models/dns_provider/digitalocean.js @@ -0,0 +1,113 @@ +'use strict'; + +const axios = require('axios'); + + +class DigitalOcean{ + static _keyMap = { + token: {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Token'}, + } + + static displayName = 'DigitalOcean' + static displayIconHtml = '' + static displayIconUni = '' + + constructor(token){ + 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`) + } + + __parseName(domain, name){ + if(name && !name.endsWith('.'+domain)){ + return `${name}.${domain}` + } + return name; + } + + get axios(){ + try{ + return axios.create({ + baseURL: 'https://api.digitalocean.com/v2/', + headers: {Authorization: `Bearer ${this.token}`} + }); + }catch(error){ + console.log(error.data); + } + } + + async listDomains(){ + try{ + let res = await this.axios.get('/domains'); + for(let domain of res.data.domains){ + domain.domain = domain.name + } + return res.data.domains; + }catch{} + } + + async getRecords(domain, options={}){ + this.__typeCheck(options.type); + let res = await this.axios.get(`/domains/${domain}/records`, {params: options}) + let records = []; + + for(let record of res.data.domain_records){ + let matchCount = 0 + for(let key in options){ + if(record[key] === options[key] && ++matchCount === Object.keys(options).length){ + records.push(record) + } + } + } + return records; + } + + async createRecord(domain, options){ + this.__typeCheck(options.type); + if(!options.data) throw new Error(`${this.constructor.name} API: 'data' key is required for this action`) + if(options.name) options.name = this.__parseName(domain, options.name); + + let res = await this.axios.post(`/domains/${domain}/records`, options); + return res.data; + } + + async deleteRecordById(domain, id){ + let res = await this.axios.delete(`/domains/${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; + + +if(require.main === module){(async function(){try{ + // const conf = require('../conf'); + + // console.log(await digi.listDomains()) + + // console.log('make', await digi.createRecord('rm-rf.stream', {name:'_test', data: '890', type: "TXT"})) + // console.log('delete', await digi.deleteRecords('rm-rf.stream', {type: 'TXT'})) + // console.log('get', await digi.getRecords('rm-rf.stream', {type:'TXT', data:'890'})) + + + // let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); + + // console.log(await porkBun.listDomains()) + + // console.log(await porkBun.deleteRecordById('holycore.quest', '415509355')) + // console.log('IIFE', await porkBun.createRecordForce('holycore.quest', {type:'A', name: 'testapi', content: '127.0.0.5'})) + // console.log('IIFE', await porkBun.getRecords('holycore.quest', {type:'A', name: 'testapi'})) +}catch(error){ + console.log('IIFE Error:', error) +}})()} From cb87f22d983865c37036d5eec7ed8961525f80cb Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sun, 11 Aug 2024 12:24:42 -0400 Subject: [PATCH 03/21] DNS API error message --- nodejs/models/dnsProvider.js | 84 +++++++++++----------- nodejs/models/dns_provider/common.js | 25 +++++++ nodejs/models/dns_provider/digitalocean.js | 40 +++++------ nodejs/models/dns_provider/porkbun.js | 7 +- nodejs/routes/dns.js | 31 +------- nodejs/views/dns.ejs | 21 ++---- 6 files changed, 100 insertions(+), 108 deletions(-) create mode 100644 nodejs/models/dns_provider/common.js diff --git a/nodejs/models/dnsProvider.js b/nodejs/models/dnsProvider.js index 1b7f810..afbade9 100644 --- a/nodejs/models/dnsProvider.js +++ b/nodejs/models/dnsProvider.js @@ -55,7 +55,10 @@ class DnsProvider extends Table{ } static __intraModel(provider){ - if(!Object.keys(providers).includes(provider)) throw new Error('Invalid DNS provider') + if(!Object.keys(providers).includes(provider)){ + throw new Error('Invalid DNS provider'); + } + let Provider = providers[provider]; let _keyMap = {...this._keyMap, ...Provider._keyMap}; @@ -67,6 +70,27 @@ class DnsProvider extends Table{ })[this.name]; } + static async create(data, ...args){ + let __intraModel = this.__intraModel(data.dnsProvider) + let provider = new __intraModel.Provider(data); + let res = await provider.listDomains(); + + let instance = await super.create.call(__intraModel, data); + if(data.noUpdate !== false) await instance.updateDomains(); + + return instance; + } + + static async get(data, ...args){ + let instance = await super.get(data); + let __intraModel = this.__intraModel(instance.dnsProvider) + + instance = await super.get.call(__intraModel, data); + instance.provider = new __intraModel.Provider(instance); + + return instance; + } + static listProviders(){ let out = []; for(let provider in providers){ @@ -81,67 +105,45 @@ class DnsProvider extends Table{ return out; } - static async create(data, ...args){ - let __intraModel = this.__intraModel(data.dnsProvider) - - let instance = await super.create.call(__intraModel, data); - if(!data.noUpdate) instance.updateDomains(); - return instance; - } - - static async get(data, ...args){ - let instance = await super.get(data); - let __intraModel = this.__intraModel(instance.dnsProvider) - - instance = await super.get.call(__intraModel, data); - instance.provider = new __intraModel.Provider(instance); - // instance.domains = Domain.getByProviderId(this.id); - - return instance; - } - async updateDomains(){ - try{ - let domains = await this.provider.listDomains(); - console.log('got domains', domains) - for(let domain of domains){ - if(!(await Domain.exists(domain.domain))){ - await Domain.create({ - created_by: this.created_by, - domain: domain.domain, - dnsProvider_id: this.id - }); - } + let domains = await this.provider.listDomains(); + for(let domain of domains){ + if(!(await Domain.exists(domain.domain))){ + await Domain.create({ + created_by: this.created_by, + domain: domain.domain, + dnsProvider_id: this.id + }); } - }catch(error){ - console.error('updateDomains error', error) } } async getDomains(){ - return Domains.getByProviderId(this.id) + return Domain.getByProviderId(this.id); } async remove(){ - let id = this.id - let instance = await super.remove() - // get all domains for this provider and delete them + let id = this.id; + let instance = await super.remove(); + for(let domain of await this.getDomains()){ + await domain.remove(); + } - return instance + return instance; } toJSON(){ return { ...super.toJSON(), domains: this.domains, - } + }; } } Domain = ModelPs(Domain); DnsProvider = ModelPs(DnsProvider); -module.exports = {Domain, DnsProvider} +module.exports = {Domain, DnsProvider}; if(require.main === module){(async function(){try{ const conf = require('../conf'); @@ -150,5 +152,5 @@ if(require.main === module){(async function(){try{ // console.log(aw) }catch(error){ - console.log('IIFE Error:', 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..59bfb3f --- /dev/null +++ b/nodejs/models/dns_provider/common.js @@ -0,0 +1,25 @@ +'use strict'; + +let dnsErrors = { + unauthorized(instance){ + let error = new Error('UnauthorizedDnsApi'); + error.name = 'UnauthorizedDnsApi'; + error.message = `Unauthorized call to ${instance.constructor ? instance.constructor.name : instance.name}`; + error.status = 424; + + return error; + }, + other(instance, status, message){ + let error = new Error('OtherDnsApiError'); + error.name = 'OtherDnsApiError'; + error.message = `DNS API Error ${instance.constructor ? instance.constructor.name : instance.name}: `; + error.status = 424; + + return error; + }, +} + +module.exports = { + dnsErrors +}; + diff --git a/nodejs/models/dns_provider/digitalocean.js b/nodejs/models/dns_provider/digitalocean.js index 8ff5306..25c7aaf 100644 --- a/nodejs/models/dns_provider/digitalocean.js +++ b/nodejs/models/dns_provider/digitalocean.js @@ -1,7 +1,7 @@ 'use strict'; const axios = require('axios'); - +const {dnsErrors} = require('./common'); class DigitalOcean{ static _keyMap = { @@ -28,30 +28,34 @@ class DigitalOcean{ return name; } - get axios(){ + async axios(method, ...args){ try{ - return axios.create({ + let a = axios.create({ baseURL: 'https://api.digitalocean.com/v2/', headers: {Authorization: `Bearer ${this.token}`} }); + + return await a[method](...args); }catch(error){ - console.log(error.data); + if(!error.response) throw error; + if(error.response.data || error.response.data.id === 'Unauthorized'){ + throw dnsErrors.unauthorized(this); + } + throw dnsErrors.other(this, error.response.status, error.response.data.message); } } async listDomains(){ - try{ - let res = await this.axios.get('/domains'); - for(let domain of res.data.domains){ - domain.domain = domain.name - } - return res.data.domains; - }catch{} + let res = await this.axios('get', '/domains'); + for(let domain of res.data.domains){ + domain.domain = domain.name + } + return res.data.domains; } async getRecords(domain, options={}){ this.__typeCheck(options.type); - let res = await this.axios.get(`/domains/${domain}/records`, {params: options}) + let res = await this.axios('get', `/domains/${domain}/records`, {params: options}) let records = []; for(let record of res.data.domain_records){ @@ -70,12 +74,12 @@ class DigitalOcean{ if(!options.data) throw new Error(`${this.constructor.name} API: 'data' key is required for this action`) if(options.name) options.name = this.__parseName(domain, options.name); - let res = await this.axios.post(`/domains/${domain}/records`, options); + let res = await this.axios('post', `/domains/${domain}/records`, options); return res.data; } async deleteRecordById(domain, id){ - let res = await this.axios.delete(`/domains/${domain}/records/${id}`); + let res = await this.axios('delete', `/domains/${domain}/records/${id}`); } async deleteRecords(domain, options){ @@ -100,14 +104,6 @@ if(require.main === module){(async function(){try{ // console.log('delete', await digi.deleteRecords('rm-rf.stream', {type: 'TXT'})) // console.log('get', await digi.getRecords('rm-rf.stream', {type:'TXT', data:'890'})) - - // let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); - - // console.log(await porkBun.listDomains()) - - // console.log(await porkBun.deleteRecordById('holycore.quest', '415509355')) - // console.log('IIFE', await porkBun.createRecordForce('holycore.quest', {type:'A', name: 'testapi', content: '127.0.0.5'})) - // console.log('IIFE', await porkBun.getRecords('holycore.quest', {type:'A', name: 'testapi'})) }catch(error){ console.log('IIFE Error:', error) }})()} diff --git a/nodejs/models/dns_provider/porkbun.js b/nodejs/models/dns_provider/porkbun.js index 23c3d8f..9e072ea 100644 --- a/nodejs/models/dns_provider/porkbun.js +++ b/nodejs/models/dns_provider/porkbun.js @@ -1,6 +1,7 @@ 'use strict'; const axios = require('axios'); +const {dnsErrors} = require('./common'); class PorkBun{ @@ -28,7 +29,11 @@ class PorkBun{ return res; }catch(error){ - throw new Error(`PorkPun API ${error.response.status}: ${error.response.data.message}`) + if(!error.response) throw error; + if(error.response.data.message.includes('Invalid API key')){ + throw dnsErrors.unauthorized(this); + } + throw dnsErrors.other(this, error.response.status, error.response.data.message) } } diff --git a/nodejs/routes/dns.js b/nodejs/routes/dns.js index 3387968..9a9c502 100644 --- a/nodejs/routes/dns.js +++ b/nodejs/routes/dns.js @@ -51,7 +51,6 @@ router.get('/domain', async function(req, res, next){ router.get('/domain/byProvider/:item', async function(req, res, next){ try{ - console.log('byProvider', req.params.item, await Domain.getByProviderId(req.params.item)) return res.json({ results: await Domain.getByProviderId(req.params.item) }); @@ -63,27 +62,12 @@ router.get('/domain/byProvider/:item', async function(req, res, next){ router.post('/domain/refresh/:item', async function(req, res, next){ try{ let item = await Model.get(req.params.item); - item.updateDomains(); - return res.json({}); + return res.json({results: await item.updateDomains()}); }catch(error){ next(error); } }) - - -router.get('/lookup/:item', async function(req, res, next){ - try{ - return res.json({ - string: req.params.item, - results: await Model.lookUp(req.params.item), - }); - - }catch(error){ - return next(error); - } -}); - router.get('/:item', async function(req, res, next){ try{ @@ -129,17 +113,4 @@ router.delete('/:item', async function(req, res, next){ } }); -router.put('/:item/renew', async function(req, res, next){ - try{ - let item = await Model.get(req.params.item); - item.createWildcardCert(); - - return res.json({ - message: `Requesting wildcard cert for ${req.params.item}`, - }) - }catch(error){ - next(error); - } -}); - module.exports = router; diff --git a/nodejs/views/dns.ejs b/nodejs/views/dns.ejs index 4918deb..eadd44d 100644 --- a/nodejs/views/dns.ejs +++ b/nodejs/views/dns.ejs @@ -35,14 +35,14 @@ function providerGet(cb){ app.api.options('dns', function(error, res){ for(let provider of res.results){ - $.scope.providerSelect.push(provider) + $.scope.providerSelect.push(provider); for(let field in provider.fields){ $.scope.providerField.push({ ...provider.fields[field], keyName: field, provider: provider.name - }) + }); } } }); @@ -127,9 +127,7 @@
-
+
- +
From e86de04a6993d6383a1c38f1d0c1ef8352cc0ad7 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sun, 11 Aug 2024 22:57:18 -0400 Subject: [PATCH 04/21] Better UI for DNS --- nodejs/models/dnsProvider.js | 4 +- nodejs/models/dns_provider/digitalocean.js | 7 +- nodejs/models/dns_provider/porkbun.js | 17 ++++ nodejs/package-lock.json | 2 +- nodejs/package.json | 1 + nodejs/public/lib/js/jq-repeat_new.js | 106 ++++++++++++++++++--- nodejs/routes/render.js | 2 +- nodejs/views/dns.ejs | 69 +++++++------- nodejs/views/hosts.ejs | 4 +- nodejs/views/top.ejs | 54 +++++++++-- 10 files changed, 210 insertions(+), 56 deletions(-) diff --git a/nodejs/models/dnsProvider.js b/nodejs/models/dnsProvider.js index afbade9..43e16d6 100644 --- a/nodejs/models/dnsProvider.js +++ b/nodejs/models/dnsProvider.js @@ -135,7 +135,9 @@ class DnsProvider extends Table{ toJSON(){ return { ...super.toJSON(), - domains: this.domains, + displayName: this.provider.constructor.displayName, + displayIconHtml: this.provider.constructor.displayIconHtml, + displayIconUni: this.provider.constructor.displayIconUni, }; } } diff --git a/nodejs/models/dns_provider/digitalocean.js b/nodejs/models/dns_provider/digitalocean.js index 25c7aaf..8b32add 100644 --- a/nodejs/models/dns_provider/digitalocean.js +++ b/nodejs/models/dns_provider/digitalocean.js @@ -9,7 +9,12 @@ class DigitalOcean{ } static displayName = 'DigitalOcean' - static displayIconHtml = '' + static displayIconHtml = ` + + + +` + // '' static displayIconUni = '' constructor(token){ diff --git a/nodejs/models/dns_provider/porkbun.js b/nodejs/models/dns_provider/porkbun.js index 9e072ea..fd0e336 100644 --- a/nodejs/models/dns_provider/porkbun.js +++ b/nodejs/models/dns_provider/porkbun.js @@ -10,6 +10,23 @@ class PorkBun{ 'secretApiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Secret key'}, } + static displayName = 'DigitalOcean'; + static displayIconHtml = ` + + + + + + +` + // ''; + static displayIconUni = ''; + baseUrl = 'https://api.porkbun.com/api/json/v3'; constructor(args){ 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/lib/js/jq-repeat_new.js b/nodejs/public/lib/js/jq-repeat_new.js index fb8e16e..f5663ee 100644 --- a/nodejs/public/lib/js/jq-repeat_new.js +++ b/nodejs/public/lib/js/jq-repeat_new.js @@ -10,7 +10,7 @@ MIT license $.scope = {}; } - var make = function(element){ + var make = function(element, template){ var result = []; result.splice = function(inputValue, ...args){ @@ -82,7 +82,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,7 +217,7 @@ 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); @@ -228,6 +228,8 @@ MIT license return this[this.indexOf(key, value)]; } + // User definable helper methods + result.__put = function($el, item, list){ $el.show(); }; @@ -241,6 +243,58 @@ MIT license $el.show(); }; + 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.__jqParent ? $.scope[this.__jqParent][this.__jqParentIndex] : undefined, + }; + }; + + result.__parseNestedTemplates = function(index, data){ + let templates = [] + let tempData = { + ...data, + _parent: data, + }; + + for(let idx in this.nestedTemplates){ + let $el = $(`${this.nestedTemplates[idx]}`); + + $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', 'update', 'parseData']){ + Object.defineProperty(result, prop, { + enumerable: false, + get(){ + return this[`__${prop}`] + }, + set(value) { + this[`__${prop}`] = value; + }, + }); + } + + + + + + result.__setPut = function(fn) { Object.defineProperty(this, '__put', { value: fn, @@ -268,10 +322,25 @@ MIT license }); }; - var $this = $( element ); + 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.__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( ' diff --git a/nodejs/views/hosts.ejs b/nodejs/views/hosts.ejs index 255aa10..67d16e5 100755 --- a/nodejs/views/hosts.ejs +++ b/nodejs/views/hosts.ejs @@ -359,7 +359,7 @@ - +
- + + + +
@@ -379,7 +379,7 @@
{{{ wildcard_text }}} diff --git a/nodejs/views/top.ejs b/nodejs/views/top.ejs index 82fcba8..d14cfbb 100755 --- a/nodejs/views/top.ejs +++ b/nodejs/views/top.ejs @@ -13,7 +13,7 @@ - + @@ -31,12 +31,56 @@ + + + + + +
{{{ forcessl_text }}}{{ host }} + {{#domain.provider}} +
+ {{displayName}} + {{/domain.provider}}
{{{ targetssl_text }}}{{ ip }}:{{ targetPort }} diff --git a/nodejs/views/top.ejs b/nodejs/views/top.ejs index d14cfbb..b744441 100755 --- a/nodejs/views/top.ejs +++ b/nodejs/views/top.ejs @@ -31,55 +31,11 @@ - - - - - - -
+ From 9cf9c76c12629226082a2cdce819ce198add4fcf Mon Sep 17 00:00:00 2001 From: William Mantly Date: Thu, 15 Aug 2024 18:49:08 -0400 Subject: [PATCH 15/21] Added relations to redis models --- nodejs/utils/model_pubsub.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nodejs/utils/model_pubsub.js b/nodejs/utils/model_pubsub.js index 8e0b823..e24c8c1 100644 --- a/nodejs/utils/model_pubsub.js +++ b/nodejs/utils/model_pubsub.js @@ -37,14 +37,15 @@ function ModelPs(model){ res.then(function(res){ publish(propKey, res, ...args); }).catch(function(error){ - console.log('toDo, publish errors...') + + console.log('toDo, publish errors...'); }); }else{ - publish(propKey, res, ...args) + publish(propKey, res, ...args); } return res; }catch(error){ - console.log("grrrr", error) + console.log("toDo, publish errors..."); } } } else { From c1cfa8bb826a7783c437caf2b34a1ac25dad633b Mon Sep 17 00:00:00 2001 From: William Mantly Date: Thu, 15 Aug 2024 18:49:42 -0400 Subject: [PATCH 16/21] Added relations to redis models --- nodejs/utils/object_validate.js | 38 +++++++++++++++++++++++++-------- nodejs/utils/redis_model.js | 20 +++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/nodejs/utils/object_validate.js b/nodejs/utils/object_validate.js index 35c3d21..1958802 100644 --- a/nodejs/utils/object_validate.js +++ b/nodejs/utils/object_validate.js @@ -21,22 +21,37 @@ function processKeys(map, data, partial){ for(let key of Object.keys(map)){ + // Do not require "isRequired" fields for partial validation, useful for + // updates. if(!map[key].always && partial && !data.hasOwnProperty(key)) continue; + // Make sure required keys are present if(!partial && map[key].isRequired && !data.hasOwnProperty(key)){ errors.push({key, message:`${key} is required.`}); continue; - } + } + // Remove undefined keys unless they have a default option or are a + // relation + if(data[key] === undefined){ + console.log('undefined key:', key, data[key], map[key], map[key].default); + if(!map[key].default){ + if(map[key].model && !map[key].type) continue; + continue; + } + } + + // Check the type of the key if(data.hasOwnProperty(key) && map[key].type && typeof(data[key]) !== map[key].type){ errors.push({key, message:`${key} is not ${map[key].type} type.`}); continue; } - // console.log(key, data[key], map[key].default, data.hasOwnProperty(key) && data[key] !== undefined ? data[key] : returnOrCall(map[key].default)) - + // Add the key to the process object to be returned and set any default + // if the key is blank out[key] = data.hasOwnProperty(key) && data[key] !== undefined ? data[key] : returnOrCall(map[key].default); + // Check for type specific validations, ie: string length if(data.hasOwnProperty(key) && process_type[map[key].type]){ let typeError = process_type[map[key].type](map[key], data[key]); if(typeError){ @@ -47,8 +62,9 @@ function processKeys(map, data, partial){ } } + // Check for errors, throw validation error if any if(errors.length !== 0){ - throw new ObjectValidateError(errors); + throw ObjectValidateError(errors); return {__errors__: errors}; } @@ -56,6 +72,7 @@ function processKeys(map, data, partial){ } function parseFromString(map, data){ + // Use the key maps data type to return string values to native let types = { boolean: function(value){ return value === 'false' ? false : true }, number: Number, @@ -80,11 +97,14 @@ function parseToString(data){ return (types[typeof(data)] || String)(data); } -function ObjectValidateError(message){ - this.name = 'ObjectValidateError'; - this.message = (message || {}); - this.keys = (message || {}) - this.status = 422; +function ObjectValidateError(keys, message){ + let error = new Error('ObjectValidateError') + error.name = "ObjectValidateError" + error.message = message || `Invalid Keys: ${message}` + error.keys = (keys || {}); + error.status = 422; + + return error } ObjectValidateError.prototype = Error.prototype; diff --git a/nodejs/utils/redis_model.js b/nodejs/utils/redis_model.js index c712598..9b62456 100644 --- a/nodejs/utils/redis_model.js +++ b/nodejs/utils/redis_model.js @@ -29,6 +29,22 @@ class QueryHelper{ } class Table{ + static errors = { + ObjectValidateError: objValidate.ObjectValidateError, + EntryNameUsed: ()=>{ + let error = new Error('EntryNameUsed'); + error.name = 'EntryNameUsed'; + error.message = `${this.prototype.constructor.name}:${data[this._key]} already exists`; + error.keys = [{ + key: this._key, + message: `${this.prototype.constructor.name}:${data[this._key]} already exists` + }] + error.status = 409; + + return error; + } + } + static redisClient = client; static models = {} @@ -139,6 +155,10 @@ class Table{ return out; } + static findall(...args){ + return this.listDetail(...args); + } + static async create(data){ // Add a entry to this redis table. try{ From ce814c306b1c2145424cfaa44c74b1753812d69b Mon Sep 17 00:00:00 2001 From: William Mantly Date: Thu, 15 Aug 2024 18:50:52 -0400 Subject: [PATCH 17/21] Fixed title whe in dev env --- nodejs/routes/render.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodejs/routes/render.js b/nodejs/routes/render.js index 7429f0d..b9876fc 100644 --- a/nodejs/routes/render.js +++ b/nodejs/routes/render.js @@ -6,7 +6,8 @@ const router = require('express').Router(); const conf = require('../conf'); const values ={ - title: conf.environment !== 'production' ? `` : '' + title: conf.environment !== 'production' ? `dev` : '', + titleIcon: conf.environment !== 'production' ? `` : '', } // List of front end node modules to be served From 17f8bec34d642879f1f6d1dab13e445861ecc3d5 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Thu, 15 Aug 2024 18:51:36 -0400 Subject: [PATCH 18/21] Polished off DNS api models --- nodejs/models/dns_provider.js | 100 +++++++++++++-------- nodejs/models/dns_provider/cloudflare.js | 86 +++++++++--------- nodejs/models/dns_provider/common.js | 97 ++++++++++++++------ nodejs/models/dns_provider/digitalocean.js | 66 ++++---------- nodejs/models/dns_provider/porkbun.js | 98 +++++++++----------- nodejs/models/host.js | 41 +++++++-- 6 files changed, 276 insertions(+), 212 deletions(-) diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js index 16c6a38..76af706 100644 --- a/nodejs/models/dns_provider.js +++ b/nodejs/models/dns_provider.js @@ -31,7 +31,7 @@ class Domain extends Table{ try{ domain = tldExtract(domain).domain; }catch{} - + let instance = await super.get(domain, ...args); return instance; @@ -47,16 +47,17 @@ class Domain extends Table{ return results; } - // toJSON(){ - // return { - // ...super.toJSON(), - // provider: { - // displayName: this.provider.constructor.displayName, - // displayIconUni: this.provider.constructor.displayIconUni, - // displayIconHtml: this.provider.constructor.displayIconHtml, - // } - // } - // } + 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)) @@ -82,24 +83,47 @@ class DnsProvider extends Table{ let Provider = providers[provider]; let _keyMap = {...this._keyMap, ...Provider._keyMap}; - return ({ + let cls = ({ [this.name] : class extends this { static _keyMap = _keyMap; static Provider = Provider; } })[this.name]; + + Object.assign(cls.prototype, Provider) + + return cls } static async create(data, ...args){ - let __intraModel = this.__intraModel(data.dnsProvider) - let provider = new __intraModel.Provider(data, ...args); + let Provider; + try{ + let __intraModel = this.__intraModel(data.dnsProvider); + Provider = __intraModel.Provider; - return await super.create.call(__intraModel, data, ...args); + // 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) + let __intraModel = this.__intraModel(instance.dnsProvider); return await super.get.call(__intraModel, data, ...args); } @@ -118,30 +142,33 @@ class DnsProvider extends Table{ return out; } - async updateDomains(){ - let domains = await this.provider.listDomains(); + get api(){ + return new this.constructor.Provider(this); + } + + async listDomains(){ + return this.api.listDomains() + } + + async updateDomains(domains){ + domains = domains || await this.listDomains(); for(let domain of domains){ if(!(await Domain.exists(domain.domain))){ await Domain.create({ created_by: this.created_by, domain: domain.domain, dnsProvider_id: this.id, - zoneId: domain.id, + zoneId: domain.zoneId, }); } } } - async getDomains(){ - return Domain.getByProviderId(this.id); - } - async remove(){ - let id = this.id; - let instance = await super.remove(); - for(let domain of await this.getDomains()){ + for(let domain of await this.domains){ await domain.remove(); } + let instance = await super.remove(); return instance; } @@ -160,22 +187,25 @@ DnsProvider.register(ModelPs(DnsProvider)) if(require.main === module){(async function(){try{ const conf = require('../conf'); - console.log(Table.models) + // console.log(await DnsProvider.findall()); - // console.log(await Domain.listDetail()) + let provider = await DnsProvider.get('e8443e03ac503c7b'); - // console.log(JSON.stringify(await Domain.get('ipa.wtf'), null, 2)) + console.log(await provider.listDomains()) - // console.log(JSON.stringify(await Domain.listDetail() ,null, 2)) - console.log(JSON.stringify(await Table.models.DnsProvider.listDetail() ,null, 2)) + 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'})) - // console.log(await Domain.getByProviderId('e8443e03ac503c7b')); + let txtRecords = await domain.getRecords({type: 'TXT'}); + console.log(txtRecords.map(i=>`${i.name}: ${i.data}`)) + // console.log(await domain.deleteRecords({type: 'TXT'})) - // console.log(aw) - - process.exit(0) }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 index b9b77c8..c44abe8 100644 --- a/nodejs/models/dns_provider/cloudflare.js +++ b/nodejs/models/dns_provider/cloudflare.js @@ -1,7 +1,7 @@ 'use strict'; const axios = require('axios'); -const {dnsErrors, DnsApi} = require('./common'); +const {DnsApi} = require('./common'); //like the options obj will always use domain data and type // change content to data @@ -32,70 +32,78 @@ class CloudFlare extends DnsApi{ 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`) } - __parseName(domain, name){ - if(name && !name.endsWith('.'+domain)){ - return `${name}.${domain}` - } - return name; - }; - async axios(method, ...args){ try{ let a = axios.create({ baseURL: 'https://api.cloudflare.com/client/v4/zones', headers: {Authorization: `Bearer ${this.token}`} }); - // console.log(a) - // console.log(...args) + return await a[method](...args); }catch(error){ - console.log(error) if(!error.response) throw error; - if(error.response.data || error.response.data.id === 'Unauthorized'){ - throw dnsErrors.unauthorized(this); + if(error.response.data && error.response.data.errors[0].code == 10000){ + throw this.errors.unauthorized(); } - throw dnsErrors.other(this, error.response.status, error.response.data.message); + 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'); - // return res.data.result; + for(let domain of res.data.result){ domain.domain = domain.name - domain.id = domain.id + 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={}){ - this.__typeCheck(options.type); - //like the options obj will always use domain data and type - // change content to data - // change name to domain + async getRecords(domain, options){ + let res = await this.axios('get', + `${domain.zoneId}/dns_records`, + ); + let records = this.__parseRes(res.data.result); - let res = await this.axios('get', `${domain.zoneId}/dns_records`, {params: options}) - - for (let record of res.data.result){ - record.domain = record.name - record.data = record.content - } - return 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){ - this.__typeCheck(options.type); - //POST zones/:zone_identifier/dns_records - // name , type , ip / content , ttl + try{ + let res = await this.axios('post', + `${domain.zoneId}/dns_records`, + this.__parseOptions(options, ['type', 'name', 'data']) + ); - if(!options.content) throw new Error(`${this.constructor.name} API: 'data' key is required for this action`) - - - let res = await this.axios('post', `${domain.zoneId}/dns_records`, options); - return res; + return this.__parseRes([res.data.result])[0]; + }catch(error){ + if(error.APIcode == 81058){ + return (await this.getRecords(domain, options))[0]; + } + throw error; + } } @@ -106,9 +114,7 @@ class CloudFlare extends DnsApi{ async deleteRecords(domain, options){ let records = await this.getRecords(domain, options) for(let record of records){ - let res = await this.deleteRecordById(domain, record.id); - // // console.log(record) - // console.log(record.id) + } return true; diff --git a/nodejs/models/dns_provider/common.js b/nodejs/models/dns_provider/common.js index 067d451..32f88a2 100644 --- a/nodejs/models/dns_provider/common.js +++ b/nodejs/models/dns_provider/common.js @@ -1,40 +1,31 @@ 'use strict'; -let dnsErrors = { - unauthorized(instance){ - let error = new Error('UnauthorizedDnsApi'); - error.name = 'UnauthorizedDnsApi'; - error.message = `Unauthorized call to ${instance.constructor ? instance.constructor.name : instance.name}`; - error.status = 424; - - return error; - }, - other(instance, status, message){ - let error = new Error('OtherDnsApiError'); - error.name = 'OtherDnsApiError'; - error.message = `DNS API Error ${instance.constructor ? instance.constructor.name : instance.name}: `; - error.status = 424; - - return error; - }, -} +const tldExtract = require('tld-extract').parse_host; class DnsApi{ errors = { - unauthorized(instance){ + unauthorized: ()=>{ let error = new Error('UnauthorizedDnsApi'); error.name = 'UnauthorizedDnsApi'; - error.message = `Unauthorized call to ${instance.constructor ? instance.constructor.name : instance.name}`; + error.message = `Unauthorized call to ${this.constructor.name}`; error.status = 424; return error; }, - other(instance, status, message){ + 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 ${instance.constructor ? instance.constructor.name : instance.name}: `; + error.message = `DNS API Error ${this.constructor.name}: ${status} ${message}`; error.status = 424; - + error.APIcode = APIcode; return error; }, } @@ -58,13 +49,69 @@ class DnsApi{ } } + /* + No instance data should ever be shared, so just give the static level inf + */ toJSON(){ - return this.constructor.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 = { - dnsErrors, DnsApi, }; diff --git a/nodejs/models/dns_provider/digitalocean.js b/nodejs/models/dns_provider/digitalocean.js index 0b4f65c..264c827 100644 --- a/nodejs/models/dns_provider/digitalocean.js +++ b/nodejs/models/dns_provider/digitalocean.js @@ -1,7 +1,7 @@ 'use strict'; const axios = require('axios'); -const {dnsErrors, DnsApi} = require('./common'); +const {DnsApi} = require('./common'); class DigitalOcean extends DnsApi{ static _keyMap = { @@ -22,18 +22,6 @@ class DigitalOcean extends DnsApi{ 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`) - } - - __parseName(domain, name){ - if(name && !name.endsWith('.'+domain)){ - return `${name}.${domain}` - } - return name; - } - async axios(method, ...args){ try{ let a = axios.create({ @@ -44,48 +32,44 @@ class DigitalOcean extends DnsApi{ return await a[method](...args); }catch(error){ if(!error.response) throw error; - if(error.response.data || error.response.data.id === 'Unauthorized'){ - throw dnsErrors.unauthorized(this); + if(error.response.data && error.response.data.id === 'Unauthorized'){ + throw this.errors.unauthorized(); } - throw dnsErrors.other(this, error.response.status, error.response.data.message); + throw this.errors.other(error.response.status, error.response.data.message); } } async listDomains(){ let res = await this.axios('get', '/domains'); - for(let domain of res.data.domains){ - domain.domain = domain.name - } - return res.data.domains; + + return this.__parseRes(res.data.domains); } - async getRecords(domain, options={}){ - this.__typeCheck(options.type); - let res = await this.axios('get', `/domains/${domain}/records`, {params: options}) - let records = []; + 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; - for(let record of res.data.domain_records){ + return records.filter((record)=>{ let matchCount = 0 for(let key in options){ if(record[key] === options[key] && ++matchCount === Object.keys(options).length){ - records.push(record) + return true; } } - } - return records; + }); } async createRecord(domain, options){ - this.__typeCheck(options.type); - if(!options.data) throw new Error(`${this.constructor.name} API: 'data' key is required for this action`) - if(options.name) options.name = this.__parseName(domain, options.name); + options = this.__parseOptions(options, ['type', 'name', 'data']); + let res = await this.axios('post', `/domains/${domain.domain}/records`, options); - let res = await this.axios('post', `/domains/${domain}/records`, options); - return res.data; + return this.__parseRes([res.data.domain_record])[0]; } async deleteRecordById(domain, id){ - let res = await this.axios('delete', `/domains/${domain}/records/${id}`); + let res = await this.axios('delete', `/domains/${domain.domain}/records/${id}`); } async deleteRecords(domain, options){ @@ -99,17 +83,3 @@ class DigitalOcean extends DnsApi{ } module.exports = DigitalOcean; - - -if(require.main === module){(async function(){try{ - // const conf = require('../conf'); - - // console.log(await digi.listDomains()) - - // console.log('make', await digi.createRecord('rm-rf.stream', {name:'_test', data: '890', type: "TXT"})) - // console.log('delete', await digi.deleteRecords('rm-rf.stream', {type: 'TXT'})) - // console.log('get', await digi.getRecords('rm-rf.stream', {type:'TXT', data:'890'})) - -}catch(error){ - console.log('IIFE Error:', error) -}})()} diff --git a/nodejs/models/dns_provider/porkbun.js b/nodejs/models/dns_provider/porkbun.js index a536da3..64347d8 100644 --- a/nodejs/models/dns_provider/porkbun.js +++ b/nodejs/models/dns_provider/porkbun.js @@ -1,7 +1,7 @@ 'use strict'; const axios = require('axios'); -const {dnsErrors, DnsApi} = require('./common'); +const {DnsApi} = require('./common'); class PorkBun extends DnsApi{ @@ -40,15 +40,16 @@ class PorkBun extends DnsApi{ secretapikey: this.secretApiKey, apikey: this.apiKey, }; - res = await axios.post(`'https://api.porkbun.com/api/json/v3'${url}`, data); + 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 dnsErrors.unauthorized(this); + throw this.errors.unauthorized(); } - throw dnsErrors.other(this, error.response.status, error.response.data.message) + // console.error('API error:', error) + throw this.errors.other(error.response.status, error.response.data.message) } } @@ -63,63 +64,65 @@ class PorkBun extends DnsApi{ 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}`); - if(!options) return res.data.records; - if(options.data) options.content = options.data; + let records = this.__parseRes(res.data.records) + if(!options) return records; + options = this.__parseOptions(options); - if(options.type) this.__typeCheck(options.type); - if(options.name) options.name = this.__parseName(domain, options.name); - let records = []; - - for(let record of res.data.records){ + return records.filter((record)=>{ let matchCount = 0 - for(let option in options){ - if(record[option] === options[option] && ++matchCount === Object.keys(options).length){ - records.push(record) - break; + 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) } - return records; + 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}/${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); - // console.log('PorkBun.deleteRecords', records) for(let record of records){ await this.deleteRecordById(domain, record.id) } } - async createRecord(domain, options){ - this.__typeCheck(options.type); - if(!options.content) throw new Error('PorkBun API: `content` key is required for this action') - // if(options.name) options.name = this.__parseName(domain, options.name); - // console.log('PorkBun.createRecord to send:', domain, options) - - let res = await this.post(`/dns/create/${domain}`, options); - return res.data; - } - - async createRecordForce(domain, options){ - let {content, ...removed} = options; - // console.log('new options', removed) - let records = await this.getRecords(domain, removed); - // console.log('createRecordForce', records) - if(records.length){ - // console.log('calling delete on', records[0].id) - // process.exit(0) - await this.deleteRecordById(domain, records[0].id) - } - return await this.createRecord(domain, options) - } async listDomains(){ let res = await this.post(`/domain/listAll`, {"includeLabels": "yes"}); @@ -128,18 +131,3 @@ class PorkBun extends DnsApi{ } module.exports = PorkBun; - - -if(require.main === module){(async function(){try{ - const conf = require('../conf'); - - // let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); - - // console.log(await porkBun.listDomains()) - - // console.log(await porkBun.deleteRecordById('holycore.quest', '415509355')) - // console.log('IIFE', await porkBun.createRecordForce('holycore.quest', {type:'A', name: 'testapi', content: '127.0.0.5'})) - // console.log('IIFE', await porkBun.getRecords('holycore.quest', {type:'A', name: 'testapi'})) -}catch(error){ - console.log('IIFE Error:', error) -}})()} diff --git a/nodejs/models/host.js b/nodejs/models/host.js index fe9548d..ca04014 100755 --- a/nodejs/models/host.js +++ b/nodejs/models/host.js @@ -1,12 +1,18 @@ 'use strict'; const Table = require('.'); +const {Domain} = require('.').models; const ModelPs = require('../utils/model_pubsub'); const tldExtract = require('tld-extract').parse_host; const LetsEncrypt = require('../utils/letsencrypt'); const conf = require('../conf'); +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'; @@ -22,7 +28,7 @@ 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'}, @@ -33,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); @@ -83,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(); @@ -94,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'); @@ -111,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){ @@ -157,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}` : '' @@ -334,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', @@ -385,4 +408,4 @@ try{ }catch(error){ console.log('IIFE test area error:', error) } -})() \ No newline at end of file +})()} From 5d42879c48027d73ff04b44c2e776e8dc636093e Mon Sep 17 00:00:00 2001 From: William Mantly Date: Thu, 15 Aug 2024 22:08:51 -0400 Subject: [PATCH 19/21] Removed print --- nodejs/utils/object_validate.js | 1 - 1 file changed, 1 deletion(-) diff --git a/nodejs/utils/object_validate.js b/nodejs/utils/object_validate.js index 1958802..f206df9 100644 --- a/nodejs/utils/object_validate.js +++ b/nodejs/utils/object_validate.js @@ -34,7 +34,6 @@ function processKeys(map, data, partial){ // Remove undefined keys unless they have a default option or are a // relation if(data[key] === undefined){ - console.log('undefined key:', key, data[key], map[key], map[key].default); if(!map[key].default){ if(map[key].model && !map[key].type) continue; continue; From c418dd39a8b50badfba7f73f9bbcecd414dffad0 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Thu, 15 Aug 2024 22:09:17 -0400 Subject: [PATCH 20/21] removed unneeded route --- nodejs/routes/dns.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/nodejs/routes/dns.js b/nodejs/routes/dns.js index e091fb7..6fdedc4 100644 --- a/nodejs/routes/dns.js +++ b/nodejs/routes/dns.js @@ -49,17 +49,6 @@ router.get('/domain', async function(req, res, next){ } }); - -router.get('/domain/byProvider/:item', async function(req, res, next){ - try{ - return res.json({ - results: await Domain.getByProviderId(req.params.item) - }); - }catch(error){ - return next(error); - } -}); - router.post('/domain/refresh/:item', async function(req, res, next){ try{ let item = await Model.get(req.params.item); From 6e168b12b335e76dca9a649255c708ac1a1a09f2 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Thu, 15 Aug 2024 22:10:15 -0400 Subject: [PATCH 21/21] Fixed issue: Old domains not being removed on refresh --- nodejs/models/dns_provider.js | 53 +++++++++++++++-------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js index 76af706..dd8bb5c 100644 --- a/nodejs/models/dns_provider.js +++ b/nodejs/models/dns_provider.js @@ -32,19 +32,7 @@ class Domain extends Table{ domain = tldExtract(domain).domain; }catch{} - let instance = await super.get(domain, ...args); - - return instance; - } - - static async getByProviderId(id, ...args){ - let domains = await this.listDetail(...args); - let results = []; - for(let domain of domains){ - if(domain.dnsProvider_id == id) results.push(domain); - } - - return results; + return await super.get(domain, ...args); } async getRecords(...args){ @@ -60,7 +48,7 @@ class Domain extends Table{ } } -Domain.register(ModelPs(Domain)) +Domain.register(ModelPs(Domain)); class DnsProvider extends Table{ static _key = 'id'; @@ -83,16 +71,12 @@ class DnsProvider extends Table{ let Provider = providers[provider]; let _keyMap = {...this._keyMap, ...Provider._keyMap}; - let cls = ({ + return ({ [this.name] : class extends this { static _keyMap = _keyMap; static Provider = Provider; } })[this.name]; - - Object.assign(cls.prototype, Provider) - - return cls } static async create(data, ...args){ @@ -133,9 +117,6 @@ class DnsProvider extends Table{ for(let provider in providers){ out.push({ name: provider, - displayName: providers[provider].displayName || provider, - displayIconUni: providers[provider].displayIconUni || '?', - displayIconHtml: providers[provider].displayIconHtml || '', fields: providers[provider]._keyMap, }); } @@ -147,20 +128,32 @@ class DnsProvider extends Table{ } async listDomains(){ - return this.api.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(!(await Domain.exists(domain.domain))){ - await Domain.create({ - created_by: this.created_by, - domain: domain.domain, - dnsProvider_id: this.id, - zoneId: domain.zoneId, - }); + 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(); } }