diff --git a/nodejs/bin/www b/nodejs/bin/www deleted file mode 100755 index 69ccee79..00000000 --- a/nodejs/bin/www +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -var app = require('../app'); -var debug = require('debug')('proxy-api:server'); -var http = require('http'); -const conf = require('../conf'); - -/** - * Get port from environment and store in Express. - */ - -var port = normalizePort(process.env.NODE_PORT || conf.port || '3000'); -app.set('port', port); - -/** - * Create HTTP server. - */ - -var server = http.createServer(app); - -var io = require('socket.io')(server); -app.io = io; - -/** - * Listen on provided port, on all network interfaces. - */ - -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - var port = parseInt(val, 10); - - if (isNaN(port)) { - // named pipe - return val; - } - - if (port >= 0) { - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - var bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - console.log('Listening on ' + bind); - - for(let listener of app.onListen){ - listener() - } -} \ No newline at end of file diff --git a/nodejs/models/cert.js b/nodejs/models/cert.js index 1392977d..3d727b92 100644 --- a/nodejs/models/cert.js +++ b/nodejs/models/cert.js @@ -1,17 +1,503 @@ 'use strict'; -const {createClient} = require('redis'); -const client = createClient({}); +const Table = require('.'); +let models = require('./').models; +const ModelPs = require('../utils/model_pubsub'); +const crypto = require("crypto"); -client.connect(); +const tldExtract = require('tld-extract').parse_host; +const LetsEncrypt = require('../utils/letsencrypt'); +const conf = require('../conf'); -async function getCert(host){ - try{ - console.log('looking for', host); - return JSON.parse(await client.GET(`${host}:latest`)); - }catch(error){ - return {} +const letsEncrypt = new LetsEncrypt({ + directoryUrl: conf.environment === "production" ? + LetsEncrypt.AcmeClient.directory.letsencrypt.production : + LetsEncrypt.AcmeClient.directory.letsencrypt.staging, +}); + + +class CertHost extends Table{ + static _key = 'host' + static _keyMap = { + host: {isRequired: true, type: 'string', max: 500}, + cert_id: {isRequired: true, type: 'string', max: 500}, + cert: {model: 'Cert', rel:'one', localKey: 'cert_id'}, + dnsProvider: {model: 'DnsProvider', rel: 'one', localKey: 'dnsProvider_id'}, + dnsProvider_id: {type: 'string'}, + } + + static async create(data, ...args){ + let instance = await super.create(data, ...args); + await this.buildLookUpObj(); + + return instance; + } + + static lookUpObj = {}; + static __lookUpIsReady = false; + + static async buildLookUpObj(){ + /* + Build a look up tree for domain records in the redis back end to allow + complex looks with wildcards. + */ + + // Hold lookUp ready while the look up object is being built. + this.__lookUpIsReady = false; + this.lookUpObj = {}; + + + // Loop over all the hosts in the redis. + for(let host of await this.list()){ + try{ + // Spit the hosts on "." into its fragments . + let fragments = host.split('.'); + + // Hold a pointer to the root of the lookup tree. + let pointer = this.lookUpObj; + + // Walk over each fragment, popping from right to left. + while(fragments.length){ + let fragment = fragments.pop(); + + // Add a branch to the lookup at the current position + if(!pointer[fragment]){ + pointer[fragment] = {}; + } + + // Add the record(leaf) when we hit the a full host name. + // #record denotes a leaf node on this tree. + if(fragments.length === 0){ + pointer[fragment]['#record'] = await this.get(host) + } + + // Advance the pointer to the next level of the tree. + pointer = pointer[fragment]; + } + }catch(error){ + console.error(error); + } + } + + // When the look up tree is finished, remove the ready hold. + this.__lookUpIsReady = true; + + } + + static lookUp(host){ + /* + Perform a complex lookup of @host on the look up tree. + */ + + + // Hold a pointer to the root of the look up tree + let place = this.lookUpObj; + + // Hold the last passed long wild card. + let last_resort = {}; + + // Walk over each fragment of the host, from right to left + for(let fragment of host.split('.').reverse()){ + + // If a long wild card is found on this level, hold on to it + if(place['**']) last_resort = place['**']; + + // If we have a match for the current fragment, update the current pointer + // A match in the lookup tree takes priority being a more exact match. + if({...last_resort, ...place}[fragment]){ + place = {...last_resort, ...place}[fragment]; + // If we have a not exact fragment match, a wild card will do. + }else if(place['*']){ + place = place['*'] + // If no fragment can be matched, continue with the long wild card branch. + }else if(last_resort){ + place = last_resort; + } + } + + // After the tree has been traversed, see if we have leaf node to return. + if(place && place['#record']) return place['#record']; + } + + static async lookUpReady(){ + /* + Wait for the lookup tree to be built. + */ + + // Check every 5ms to see if the look up tree is ready + while(!this.__lookUpIsReady) await new Promise(r => setTimeout(r, 5)); + return true; + } +} +CertHost.register(CertHost); + +// (async function(){ +// await CertHost.buildLookUpObj(); +// })(); + +class Cert 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': {type: 'string', default: ()=>crypto.randomBytes(16).toString("hex")}, + 'name': {type: 'string', isRequired: true}, + 'is_active': {type: 'boolean', default: false}, + 'type': {isRequired: true, type: 'string', extends: true}, + 'hosts': {model: 'CertHost', rel: 'many', remoteKey: 'cert_id'}, + 'expires': {isRequired: false, type: 'number'}, + + 'has_error': {isRequired: false, type: 'boolean'}, + 'status': {isRequired: false, type: 'string', min: 3, max: 500}, + + 'cert_pem': {isPrivate: true}, + 'fullchain_pem': {isPrivate: true}, + 'privkey_pem': {isPrivate: true}, + } + + static async getByHost(host){ + try{ + let host = await CertHost.lookUp(host); + + return this.get(host.cert_id); + }catch{} + + try{ + + }catch{} + } + + static async findall(filterObj, ...args){ + let results = [...(await super.findall(filterObj, ...args))]; + + for(let [name, Cls] of Object.entries(this.__extendedModels.type)){ + if(Cls.__orginalMethods.includes('findall')){ + results.push(...(await Cls.findall(filterObj, ...args))) + } + } + + return results; + } + + static parseCert(cert){ + return LetsEncrypt.AcmeClient.crypto.readCertificateInfo(cert); + } + + static async cleanUp(){ + let countBad = 0 + for(let cert of await this.findall({type:'Auto'})){ + try{ + await models.Host.lookUp(cert.id) || await models.Host.get(cert.id); + }catch(error){ + console.log('deleting', cert.id) + await cert.remove(); + countBad++ + } + } + return countBad; + } +} +Cert.register(Cert, ModelPs); + +class Auto extends Cert{ + static _keyMap = { + hosts: {} + }; + + static async get(data, ...args){ + let index = typeof data === 'object' ? data[this._key] : data; + + let cert = JSON.parse(await this.redisClient.GET(`${index}:latest`)); + let parsed = this.parseCert(cert.cert_pem); + + return await this.__buildInstance({ + ...cert, + id: index, + is_active: true, + name: 'Auto', + hosts: [{host: index}], + type: 'Auto', + status: 'HTTP auth', + expires: cert.expiry*1000, + created_on: +parsed.notBefore, + updated_on: +parsed.notBefore, + created_by: 'System', + }); + } + + static async create(){ + throw new Error('Can not create this type of cert'); + } + + async getOthers(){ + let others = await this.constructor.redisClient.KEYS(`${this.id}:*`); + return others.filter(i=> !i.endsWith(':latest')); + } + + async update(){ + throw new Error('Can not update this type of cert'); + } + + async remove(...args){ + console.log('in remove') + try{ + for(let key of await this.getOthers()){ + await this.constructor.redisClient.DEL(key); + } + + await this.constructor.redisClient.DEL(`${this.id}:latest`); + }catch(error){ + console.log('error in remove', error); + } + } + + static async findall(filterObj, ...args){ + let results = []; + + for(let key of await this.redisClient.KEYS('*:latest')){ + let item = await this.get(key.split(':')[0]) + if(item.type !== 'Auto') continue; + + if(!filterObj) results.push(item); + let matchCount = 0; + for(let option in filterObj){ + if(item[option] === filterObj[option] && ++matchCount === Object.keys(filterObj).length){ + results.push(item); + break; + } + } + + results.push(item) + } + + return results + } + +} +Cert.extend('type', Auto); + +class Manual extends Cert{ +} + +class WildCard extends Cert{ + + async matchHostsToDns(data){ + let errors = []; + for(let idx in data.host){ + let domain; + try{ + if(data.host.slice(0,idx).includes(data.host[idx])){ + errors.push({key: `host`, keyIndex: idx, message: 'Please remove duplicate names.'}); + continue; + } + domain = await models.Domain.get(data.host[idx]); + }catch{ + errors.push({key: `host`, keyIndex: idx, message: 'No matching DNS provider.'}); + continue; + } + try{ + await this.hostsCreate({ + host: data.host[idx], + dnsProvider_id: domain.dnsProvider_id, + }); + }catch(error){ + console.log('add host error', error) + errors.push({key: `host`, keyIndex: idx, message: 'Used by another cert'}); + } + } + + if(errors.length) throw this.constructor.errors.ObjectValidateError(errors, ''); + } + + static async create(data, ...args){ + if(!Array.isArray(data.host)) data.host = [data.host]; + + let instance = await super.create({ + ...data, + status: 'Started', + }, ...args); + + try{ + await instance.matchHostsToDns(data); + await instance.startWildCardRequest(); + }catch(error){ + await instance.remove(); + throw error; + } + + return instance; + } + + async renew(data, ...args){ + await this.update({ + updated_by: data.username, + status: 'Renewing...', + }); + + try{ + await this.startWildCardRequest(); + }catch(error){ + await this.update({ + updated_by: data.username, + status: 'Renew failed', + }); + + throw error; + } + + return this; + } + + startWildCardRequest(){ + return new Promise(async(resolve, reject)=>{ + try{ + await this.createWildcardCert(resolve, reject); + }catch(error){ + reject(error); + } + }); + } + + async getPem(type){ + let types = ['cert_pem', 'fullchain_pem', 'privkey_pem']; + if(!this.is_active || !types.includes(type)) throw this.constructor.errors.EntryNotFound(this.id); + let cert = JSON.parse(await this.constructor.redisClient.GET(`${this.id}:latest`)); + return cert[type] + } + + async createWildcardCert(resolve, reject){ + try{ + let instance = this; + let cert = await letsEncrypt.dnsWildcard(this.hosts.map(i=>i.host), { + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + resolve(); + try{ + let domain = await models.Domain.get(authz.identifier.value); + let parts = tldExtract(authz.identifier.value); + + await domain.createRecord({ + type:'TXT', + name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, + data: `${keyAuthorization}` + }); + }catch(error){ + await instance.update({ + has_error: true, + status: `challengeCreateFn: ${error}`, + }); + console.log('model Host challengeCreateFn error:', error) + } + }, + onDnsCheck: async(authz, checkCount)=>{ + await instance.update({ + status: `${checkCount} Checking DNS` + }); + }, + onDnsCheckFail: async(authz, error)=>{ + await instance.update({ + has_error: true, + status: `DNS check failed for ${authz.identifier.value}` + }); + }, + onDnsCheckFound: async(authz)=>{ + resolve(); + // await instance.update({ + // status: `DNS check found ${authz.identifier.value}` + // }); + }, + onDnsCheckSuccess: async(authz)=>{ + await instance.update({ + status: `DNS check success` + }); + }, + onDnsCheckRemove: async(authz)=>{ + await instance.update({ + status: `DNS remove record ${authz.identifier.value}` + }); + }, + challengeRemoveFn: async (authz, challenge, keyAuthorization)=>{ + try{ + let domain = await models.Domain.get(authz.identifier.value); + let parts = tldExtract(authz.identifier.value); + + await domain.deleteRecords({ + type:'TXT', + name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, + data: `${keyAuthorization}` + }); + }catch(error){ + console.log('challengeRemoveFn Error:', error); + await instance.update({ + + status: `DNS remove record ${authz.identifier.value}` + }); + } + }, + }); + + resolve(); + + let toAdd = { + cert_pem: cert.cert.split('\n\n')[0], + fullchain_pem: cert.cert, + privkey_pem: cert.key.toString(), + csr_pem: cert.csr.toString(), + expiry: 4120307657, + real_expiry: +LetsEncrypt.AcmeClient.crypto.readCertificateInfo(cert.cert).notAfter/1000, + }; + + await this.constructor.redisClient.SET(`${this.id}:latest`, JSON.stringify(toAdd)); + await this.update({ + status: `Done`, + is_active: true, + expires: toAdd.real_expiry*1000, + }); + + return this; + }catch(error){ + console.log('le failed', error) + reject(error); + throw error; + // this.update({ + // wildcard_status: `LE failed` + // }); + } } } +Cert.extend('type', WildCard); + +if(require.main === module){(async function(){try{ + + let certHosts = await CertHost.findall() + console.log('CertHost', certHosts) + + console.log(await Cert.cleanUp()) + + let certs = await Cert.findall() + + console.log('Certs', certs) + + // for(let cert of certs){ + // await cert.remove() + // } + // console.log('certs', certs) + // for(let cert of certs){ + // if(cert.real_expiry) console.log(cert) + // } + + + // console.log(await Cert.get('blah.test.cl.vm42.us')) + + + // console.log(await certs[0].remove()) + // console.log(LetsEncrypt.AcmeClient.crypto.readCertificateInfo(certs[0].cert_pem)) -module.exports = {getCert}; + // console.log(certs[0].host, await Host.lookUp(certs[0].host) || await Host.get(certs[0].host)) +}catch(error){ + console.log('IIFE Error:', error); +}finally{ + process.exit(0); +}})()} diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js deleted file mode 100644 index dd8bb5c0..00000000 --- a/nodejs/models/dns_provider.js +++ /dev/null @@ -1,204 +0,0 @@ -'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 index c44abe8d..1fd06a59 100644 --- a/nodejs/models/dns_provider/cloudflare.js +++ b/nodejs/models/dns_provider/cloudflare.js @@ -1,14 +1,10 @@ 'use strict'; const axios = require('axios'); -const {DnsApi} = require('./common'); +const {DnsProvider} = require('../').models; -//like the options obj will always use domain data and type -// change content to data -// change name to domain - -class CloudFlare extends DnsApi{ +class CloudFlare extends DnsProvider{ static _keyMap = { token: {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Token'}, } @@ -22,14 +18,16 @@ class CloudFlare extends DnsApi{ // Cloud icon for cloudflare static displayIconUni = '' - constructor(token){ - super() - this.token = token.token || token; - } + /* + 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 - __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`) + What the the API calls it : What the class wants it as. + */ + __apiKeyMap = { + 'content': 'data', + 'id': 'zoneId', } async axios(method, ...args){ @@ -50,32 +48,17 @@ class CloudFlare extends DnsApi{ } async listDomains(){ - let res = await this.axios('get'); + let res = await this.axios('get'); - for(let domain of res.data.result){ - domain.domain = domain.name - domain.zoneId = domain.id - } + console.log('cf domain red', res.data, '\n\n parsed res', this.__parseRes(res.data.result)) - return res.data.result; + return this.__parseRes(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 res = await this.axios('get', + `${domain.zoneId}/dns_records`, + ); let records = this.__parseRes(res.data.result); if(!options) return records; @@ -104,7 +87,6 @@ class CloudFlare extends DnsApi{ } throw error; } - } async deleteRecordById(domain, id){ @@ -121,28 +103,4 @@ class CloudFlare extends DnsApi{ } } -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) -}})()} +DnsProvider.extend('dnsProvider', CloudFlare); diff --git a/nodejs/models/dns_provider/common.js b/nodejs/models/dns_provider/common.js deleted file mode 100644 index 32f88a2d..00000000 --- a/nodejs/models/dns_provider/common.js +++ /dev/null @@ -1,117 +0,0 @@ -'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 index 264c827b..0434cb1a 100644 --- a/nodejs/models/dns_provider/digitalocean.js +++ b/nodejs/models/dns_provider/digitalocean.js @@ -1,28 +1,37 @@ 'use strict'; const axios = require('axios'); -const {DnsApi} = require('./common'); +const {DnsProvider} = require('../').models; -class DigitalOcean extends DnsApi{ + +class DigitalOcean extends DnsProvider{ static _keyMap = { token: {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Token'}, } static displayName = 'DigitalOcean' + static displayIconUni = '' static displayIconHtml = ` ` - // '' - static displayIconUni = '' - constructor(token){ - super() - this.token = token.token || token; + + /* + 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 = { + 'name': 'domain', } async axios(method, ...args){ + console.log('this', this.constructor) try{ let a = axios.create({ baseURL: 'https://api.digitalocean.com/v2/', @@ -82,4 +91,4 @@ class DigitalOcean extends DnsApi{ } } -module.exports = DigitalOcean; +DnsProvider.extend('dnsProvider', DigitalOcean) diff --git a/nodejs/models/dns_provider/index.js b/nodejs/models/dns_provider/index.js new file mode 100644 index 00000000..14963245 --- /dev/null +++ b/nodejs/models/dns_provider/index.js @@ -0,0 +1,266 @@ +'use strict'; + +const crypto = require("crypto"); +const tldExtract = require('tld-extract').parse_host; + +const conf = require('../../conf'); +const Table = require('../'); +const ModelPs = require('../../utils/model_pubsub'); + + +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'}, + 'domains': {model:'Domain', rel: 'many', remoteKey: 'dnsProvider_id'}, + 'dnsProvider': {isRequired: true, type: 'string', extends: true}, + } + + static errors = { + ...super.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 async create(data){ + // // Test if the provided key is valid by trying to get a list of domains + // // before adding the entry to the DB + // let api = new this(data); + // console.log('api', api) + // let domains = await api.listDomains(); + + // Create the entry in the backing DB, add the fetched domains and + // return the new instance + let instance = await super.create(data); + try{ + await instance.updateDomains(); + }catch(error){ + await instance.remove(); + throw error; + } + + return instance; + } + + async updateDomains(domains){ + /* + Use the remote API to get a list of known domains from the remote + service, add them to Domain table associated with the current DNS + provider. Remove any current associated domains not returned by the + remote API call. + */ + + domains = domains || await this.listDomains(); + + // Hold a list of current domains so when remove any that are not + // returned by the current API call + let currentDomains = this.domains.map(domain => domain.domain); + + // Walk the list of domains returned by the API + for(let domain of domains){ + // Reduce the list of currentDomains known and skip to the next + // domain + if(currentDomains.includes(domain.domain)){ + delete currentDomains[currentDomains.indexOf(domain.domain)]; + continue; + } + + // Add a new Domain entry + await this.domainsCreate({ + created_by: this.created_by, + domain: domain.domain, + zoneId: domain.zoneId, + }); + } + + // Walk the list of domains left in currentDomains and remove them. + for(let domain of currentDomains){ + if(!domain) continue; + domain = await Table.models.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; + // } + + static info(){ + /* + Parse class info about the current DNS provider into something the front + end can use. + */ + let svgDataUrl = `data:image/svg+xml;charset=utf-8,${ + encodeURIComponent(this.displayIconHtml) + .replace(/'/g, '%27') + .replace(/"/g, '%22') + }`; + + return { + name: this.name, + displayName: this.displayName, + displayIconUni: this.displayIconUni, + displayIconHtml: svgDataUrl, + fields: this._keyMap, + }; + } + + static toJSON(){ + return { + ...this.info(), + ...(this.__isExtended ? {} :super.toJSON()), + }; + } + + /* + No instance data should ever be shared, so just give the static level inf + */ + toJSON(){ + return { + ...this.constructor.toJSON(), + ...super.toJSON(), + }; + } + + /* + Helper methods for DNS API interactions + */ + + __typeCheck(type){ + let validDnsRecordTypes = [ + 'A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', + 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB', + ]; + + if(!validDnsRecordTypes.includes(type)){ + // change this to throw a validation error + 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; + } +} + +DnsProvider.register(DnsProvider, ModelPs) + + +require('./cloudflare'); +require('./digitalocean'); +require('./porkbun'); + + +if(require.main === module){(async function(){try{ + + // console.log(await DnsProvider.subMobels['DigitalOcean'].get('fcaaa1f6b8ab405e')) + // console.log(JSON.stringify(DnsProvider, null, 2)); + + + let providers = await DnsProvider.findall(); + + // console.log(providers) + + console.log(JSON.stringify(providers[0], null, 2)) + + // let provider = await DnsProvider.get('cb9b1041bf92f668'); + + // await provider.update({dnsProvider: "CloudFlare"}) + + // console.log(provider) + + // 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/porkbun.js b/nodejs/models/dns_provider/porkbun.js index 64347d81..17cd6522 100644 --- a/nodejs/models/dns_provider/porkbun.js +++ b/nodejs/models/dns_provider/porkbun.js @@ -1,10 +1,10 @@ 'use strict'; const axios = require('axios'); -const {DnsApi} = require('./common'); +const {DnsProvider} = require('../').models; -class PorkBun extends DnsApi{ +class PorkBun extends DnsProvider{ static _keyMap = { 'apiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API key'}, 'secretApiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Secret key'}, @@ -26,10 +26,16 @@ class PorkBun extends DnsApi{ ` - constructor(args){ - super() - this.apiKey = args.apiKey; - this.secretApiKey = args.secretApiKey; + /* + 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 post(url, data){ @@ -53,30 +59,6 @@ class PorkBun extends DnsApi{ } } - __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) @@ -130,4 +112,4 @@ class PorkBun extends DnsApi{ } } -module.exports = PorkBun; +DnsProvider.extend('dnsProvider', PorkBun); diff --git a/nodejs/models/domain.js b/nodejs/models/domain.js new file mode 100644 index 00000000..f3c55267 --- /dev/null +++ b/nodejs/models/domain.js @@ -0,0 +1,43 @@ +'use strict'; + +const tldExtract = require('tld-extract').parse_host; +const conf = require('../conf'); +const Table = require('../utils/redis_model'); +const ModelPs = require('../utils/model_pubsub'); + + +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.getRecords(this, ...args); + } + + async createRecord(...args){ + return await this.provider.createRecord(this, ...args); + } + + async deleteRecords(...args){ + return await this.provider.deleteRecords(this, ...args); + } +} + +Domain.register(ModelPs(Domain)); diff --git a/nodejs/models/host.js b/nodejs/models/host.js index ca040140..42de48eb 100755 --- a/nodejs/models/host.js +++ b/nodejs/models/host.js @@ -1,7 +1,7 @@ 'use strict'; const Table = require('.'); -const {Domain} = require('.').models; +const {Domain, Cert} = require('.').models; const ModelPs = require('../utils/model_pubsub'); const tldExtract = require('tld-extract').parse_host; @@ -14,6 +14,7 @@ const letsEncrypt = new LetsEncrypt({ LetsEncrypt.AcmeClient.directory.letsencrypt.staging, }); + class Host extends Table{ static _key = 'host'; static _keyMap = { @@ -21,17 +22,17 @@ class Host extends Table{ '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}, - 'host': {isRequired: true, type: 'string', min: 3, max: 500}, - 'ip': {isRequired: true, type: 'string', min: 3, max: 500}, + 'host': {isRequired: true, type: 'string', min: 1, max: 500}, + 'ip': {isRequired: true, type: 'string', min: 1, max: 500}, 'targetPort': {isRequired: true, type: 'number', min:0, max:65535}, 'forcessl': {isRequired: false, default: true, type: 'boolean'}, '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}, - 'wildcard_parent': {isRequired: false, type: 'string', min: 3, max: 500}, - 'wildcard_expires': {isRequired: false, type: 'number'}, + // 'is_wildcard': {default: false, isRequired: false, type: 'boolean',}, + 'cert_id': {type: 'string', isRequired: false}, + // 'cert': {model: 'Cert', rel:'one', localKey: 'cert_id'}, 'domain': {model: 'Domain', rel: 'one'}, + 'prevent_remove': {type:'boolean', default: false}, } static lookUpObj = {}; @@ -112,108 +113,6 @@ class Host extends Table{ } } - async createWildcardCert(){ - if(!this.host.startsWith('*.')) throw new Error('not wild card'); - - try{ - let host = this; - - await host.update({ - wildcard_status: 'Requesting', - }); - let cert = await letsEncrypt.dnsWildcard(this.host, { - challengeCreateFn: async (authz, challenge, keyAuthorization) => { - await host.update({ - wildcard_status: `Adding record` - }); - try{ - let parts = tldExtract(authz.identifier.value); - - let res = await host.domain.createRecord( - { - type:'TXT', - name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, - data: `${keyAuthorization}` - } - ); - }catch(error){ - console.log('model Host challengeCreateFn error:', error) - await host.update({ - wildcard_status: `Add DNS record failed` - }); - } - }, - onDnsCheck: async(authz, checkCount)=>{ - await host.update({ - wildcard_status: `${checkCount} Checking DNS` - }); - }, - onDnsCheckFail: async(authz, error)=>{ - await host.update({ - wildcard_status: `DNS check failed for ${authz.identifier.value}` - }); - }, - onDnsCheckFound: async(authz)=>{ - await host.update({ - wildcard_status: `DNS check found` - }); - }, - onDnsCheckSuccess: async(authz)=>{ - await host.update({ - wildcard_status: `DNS check success` - }) - }, - onDnsCheckRemove: async(authz)=>{ - await host.update({ - wildcard_status: `DNS remove record` - }) - }, - challengeRemoveFn: async (authz, challenge, keyAuthorization)=>{ - await host.update({ - wildcard_status: `DNS remove record` - }) - try{ - let parts = tldExtract(authz.identifier.value); - await host.domain.deleteRecords( - { - type:'TXT', - name: `_acme-challenge${parts.sub ? `.${parts.sub}` : '' - }`, - content: `${keyAuthorization}`} - ); - - }catch(error){ - await host.update({ - wildcard_status: `DNS remove record failed for ${authz.identifier.value}` - }) - } - }, - }); - - let toAdd = { - cert_pem: cert.cert.split('\n\n')[0], - fullchain_pem: cert.cert, - privkey_pem: cert.key.toString(), - csr_pem: cert.csr.toString(), - expiry: 4120307657, - real_expiry: +LetsEncrypt.AcmeClient.crypto.readCertificateInfo(cert.cert).notAfter/1000, - } - - - await this.constructor.redisClient.SET(`${this.host}:latest`, JSON.stringify(toAdd)); - await this.update({ - wildcard_status: `Done`, - wildcard_expires: toAdd.real_expiry*1000, - }); - - return this; - }catch(error){ - console.log('le failed', error) - this.update({ - wildcard_status: `LE failed` - }); - } - } async update(...args){ try{ @@ -227,8 +126,12 @@ class Host extends Table{ } } - async remove(...args){ + async remove(force, ...args){ try{ + if(this.prevent_remove && !force){ + // throw error or something here + return; + } let out = await super.remove(...args); await Host.buildLookUpObj(); await this.bustCache(this.host); @@ -333,7 +236,7 @@ class Host extends Table{ return true; } } -Host.register(ModelPs(Host)) +Host.register(ModelPs(Host)); class Cached extends Table{ @@ -344,19 +247,20 @@ class Cached extends Table{ } } -(async function(){ - await Host.buildLookUpObj(); -})(); +// (async function(){ +// await Host.buildLookUpObj(); +// })(); -module.exports = {Host: ModelPs(Host)}; if(require.main === module){(async function(){ try{ - await Host.lookUpReady(); - let host = await Host.get('*.new.test.wtf') + // await Host.lookUpReady(); + console.log(JSON.stringify(Host, null, 2)) + + // let host = await Host.get('*.new.test.wtf') - console.log('host', host.domain.provider.api); + // console.log('host', host.domain.provider.api); diff --git a/nodejs/models/index.js b/nodejs/models/index.js index 36b62cc8..5ce275e5 100644 --- a/nodejs/models/index.js +++ b/nodejs/models/index.js @@ -7,3 +7,5 @@ require('./dns_provider'); require('./host'); require('./token'); require('./user'); +require('./domain'); +require('./cert'); diff --git a/nodejs/models/user_ldap.js b/nodejs/models/user_ldap.js index 0c02db6d..5da86745 100644 --- a/nodejs/models/user_ldap.js +++ b/nodejs/models/user_ldap.js @@ -9,162 +9,65 @@ const client = new Client({ }); -const user_parse = function(data){ - if(data[conf.userNameAttribute]){ - data.username = data[conf.userNameAttribute] - delete data[conf.userNameAttribute]; - } - - if(data.uidNumber){ - data.uid = data.uidNumber; - delete data.uidNumber; - } +class LdapModel extends Model{ + static modelBacking = LdapModel; + static client = client; - return data; } -var User = {} -User.backing = "LDAP"; - -User.keyMap = { - 'username': {isRequired: true, type: 'string', min: 3, max: 500}, - 'password': {isRequired: true, type: 'string', min: 3, max: 500}, -} - -User.list = async function(){ - try{ - await client.bind(conf.bindDN, conf.bindPassword); - - const res = await client.search(conf.searchBase, { - scope: 'sub', - filter: conf.userFilter, - }); - - await client.unbind(); - - return res.searchEntries.map(function(user){return user.uid}); - }catch(error){ - throw error; - } -}; - -User.listDetail = async function(){ - try{ - await client.bind(conf.bindDN, conf.bindPassword); - - const res = await client.search(conf.searchBase, { - scope: 'sub', - filter: conf.userFilter, - }); - - await client.unbind(); - - let users = [] - - for(let user of res.searchEntries){ - let obj = Object.create(this); - Object.assign(obj, user_parse(user)); - - users.push(obj) +class User extends Model{ + static user_parse(data){ + if(data[conf.userNameAttribute]){ + data.username = data[conf.userNameAttribute] + delete data[conf.userNameAttribute]; } - return users; + if(data.uidNumber){ + data.uid = data.uidNumber; + delete data.uidNumber; + } - }catch(error){ - throw error; + return data; } -}; - -User.get = async function(data){ - try{ - if(typeof data !== 'object'){ - let username = data; - data = {}; - data.username = username; - } - - await client.bind(conf.bindDN, conf.bindPassword); - let filter = `(&${conf.userFilter}(${conf.userNameAttribute}=${data.username}))`; + static async get(data, ...args){ + try{ + if(typeof index === 'object'){ + index = index[this._key]; + } - const res = await client.search(conf.searchBase, { - scope: 'sub', - filter: filter, - }); + await this.client.bind(conf.bindDN, conf.bindPassword); - await client.unbind(); + const res = await this.client.search(conf.searchBase, { + scope: 'sub', + filter: `(&${conf.userFilter}(${conf.userNameAttribute}=${data.username}))`, + }); - let user = res.searchEntries[0] + await this.client.unbind(); - if(user){ - let obj = Object.create(this); - Object.assign(obj, user_parse(user)); + if(!res.searchEntries[0]) throw this.errors.EntryNotFound(index) - return obj; - }else{ - let error = new Error('UserNotFound'); - error.name = 'UserNotFound'; - error.message = `LDAP:${data.username} does not exists`; - error.status = 404; + return this(user_parse(res.searchEntries[0])); + }catch(error){ throw error; } - }catch(error){ - throw error; } -}; - -User.exists = async function(data){ - // Return true or false if the requested entry exists ignoring error's. - try{ - await this.get(data); - return true - }catch(error){ - return false; - } -}; + static async login(data){ + try{ + let user = await this.get(data.username); -User.invite = async function(){ - try{ - let token = await InviteToken.add({created_by: this.username}); - - return token; - - }catch(error){ - throw error; - } -}; + await client.bind(user.dn, data.password); -User.login = async function(data){ - try{ - let user = await this.get(data.username); + await client.unbind(); - await client.bind(user.dn, data.password); - - await client.unbind(); - - return user; - - }catch(error){ - throw error; - } -}; + return user; + }catch(error){ + throw error; + } + }; +} -module.exports = {User}; - - -// (async function(){ -// try{ -// console.log(await User.list()); - -// console.log(await User.listDetail()); - -// console.log(await User.get('wmantly')) - -// }catch(error){ -// console.error(error) -// } -// })() \ No newline at end of file diff --git a/nodejs/public/lib/js/app-base.js b/nodejs/public/lib/js/app-base.js index 486cb0c9..8c56d598 100644 --- a/nodejs/public/lib/js/app-base.js +++ b/nodejs/public/lib/js/app-base.js @@ -302,6 +302,8 @@ app.util = (function(app){ $target.html(message).slideDown('fast'); } setTimeout(callback,10) + + return $target; } $.fn.serializeObject = function(){ @@ -383,18 +385,20 @@ $( document ).ready(function(){ $('.momentFromNow').each((idx, el)=>{ var $el = $(el); try{ - $el.html(moment($(el).data('date')).fromNow()); + let date = $(el).data('date'); + if(data) $el.html(moment(date).fromNow()); }catch{} }) }, 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'}); }; @@ -412,27 +416,25 @@ function formAJAX(btn){ return false; } - app.util.actionMessage( + var $actionTarget = app.util.actionMessage( '