From ecc8263a36d4682f1ec1f93ac7da2558e0aa8892 Mon Sep 17 00:00:00 2001 From: Florian Mertens Date: Tue, 8 Jun 2021 19:29:21 +0100 Subject: [PATCH] Explicitly log in to QB to avoid getting locked out due to suspicious activity, plus added support for user_token authentication --- README.md | 16 ++++- bin/qb-deploy.js | 9 ++- bin/qb-init.js | 15 ++++- demo/index.html | 4 +- demo/static/bundle.js | 3 + demo/static/main.css | 4 ++ lib/api.js | 149 +++++++++++++++++++++++++++++++++-------- lib/generate-config.js | 24 ++++++- 8 files changed, 188 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 9808fd2..643959e 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ Below are the prompts (see the [Notes](#notes) below for an important advisory r ```javascript { name: 'username', - message: 'QuickBase username:' + message: 'QuickBase username (leave blank to use the QUICKBASE_CLI_USERNAME environment variable):' }, { name: 'password', - message: 'QuickBase password (Leave blank to use the QUICKBASE_CLI_PASSWORD env variable):' + message: 'QuickBase password (leave blank to use the QUICKBASE_CLI_PASSWORD environment variable):' }, { name: 'dbid', @@ -47,11 +47,19 @@ Below are the prompts (see the [Notes](#notes) below for an important advisory r }, { name: 'appToken', - message: 'QuickBase application token (if applicable):' + message: 'QuickBase application token (if applicable) (leave blank to use the QUICKBASE_CLI_APPTOKEN environment variable):' +}, +{ + name: 'userToken', + message: 'QuickBase user token (if applicable) (leave blank to use the QUICKBASE_CLI_USERTOKEN environment variable):' }, { name: 'appName', message: 'Code page prefix (leave blank to disable prefixing uploaded pages):' +}, +{ + name: 'ticketExpiryHours', + message: 'Ticket expiry period in hours (default is 1):' } ``` @@ -92,6 +100,8 @@ For now this is only a wrapper around `git clone`. After you pull down a repo yo * Instead of exposing your password for the `quickbase-cli.config.js` file you can rely on an environment variable called `QUICKBASE_CLI_PASSWORD`. If you have that variable defined and leave the `password` empty when prompted the `qb deploy` command will use it instead. Always practice safe passwords. +* The same can also be done with username (using `QUICKBASE_CLI_USERNAME`), user token (using `QUICKBASE_CLI_USERTOKEN`) and/or app token (using `QUICKBASE_CLI_APPTOKEN`). + * ~~Moves are being made to add cool shit like a build process, global defaults, awesome starter templates, and pulling down existing code files from QuickBase. They're not out yet, so for now you're on your own.~~ * I no longer work with QuickBase applications, so the cool shit I had planned won't happen unless someone submits some dope pull requests. diff --git a/bin/qb-deploy.js b/bin/qb-deploy.js index b9075bf..db4f611 100644 --- a/bin/qb-deploy.js +++ b/bin/qb-deploy.js @@ -43,7 +43,14 @@ if (program.watch) { async function qbDeploy(source) { console.log('Uploading files to QuickBase...'); - + + try { + await api.authenticateIfNeeded(); + } catch(e) { + console.error(e); + return; + } + const stats = await fs.statSync(source); const isFile = stats.isFile(); diff --git a/bin/qb-init.js b/bin/qb-init.js index 30c7e65..c694ff1 100644 --- a/bin/qb-init.js +++ b/bin/qb-init.js @@ -8,7 +8,7 @@ const QUESTIONS = [ { type: 'input', name: 'username', - message: 'QuickBase username:' + message: 'QuickBase username (leave blank to use the QUICKBASE_CLI_USERNAME environment variable):' }, { type: 'password', @@ -29,13 +29,24 @@ const QUESTIONS = [ { type: 'input', name: 'appToken', - message: 'QuickBase application token (if applicable):' + message: 'QuickBase application token (if applicable) (leave blank to use the QUICKBASE_CLI_APPTOKEN environment variable):' + }, + { + type: 'input', + name: 'userToken', + message: 'QuickBase user token (if applicable) (leave blank to use the QUICKBASE_CLI_USERTOKEN environment variable):' }, { type: 'input', name: 'appName', message: 'Code page prefix (leave blank to disable prefixing uploaded pages):' + }, + { + type: 'input', + name: 'authenticate_hours', + message: + 'Authentication expiry period in hours (default is 1):' } ]; diff --git a/demo/index.html b/demo/index.html index 51990da..f77347f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -7,7 +7,9 @@

Hello, world

- +

This is a page, that should be deployed using quickbase-cli to quickbase, with styling and scripts properly referenced.

+

If this paragraph is bold, CSS files are properly referenced.

+

If this paragraph is bold, JS files are properly referenced.

diff --git a/demo/static/bundle.js b/demo/static/bundle.js index 2b6d184..ae59fe2 100644 --- a/demo/static/bundle.js +++ b/demo/static/bundle.js @@ -1 +1,4 @@ console.log('Hello from bundle.js'); +var elements = document.getElementsByClassName('check_js'); +var checkJsElement = elements[0]; +checkJsElement.style.fontWeight = 'bold'; \ No newline at end of file diff --git a/demo/static/main.css b/demo/static/main.css index 9d23ceb..88932ae 100644 --- a/demo/static/main.css +++ b/demo/static/main.css @@ -1,3 +1,7 @@ body { font-family: sans-serif; } + +.check_css { + font-weight: bold; +} diff --git a/lib/api.js b/lib/api.js index 1267e3f..19cdf99 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,26 +1,40 @@ const https = require('https'); const URL = require('url'); + class ApiClient { + constructor(config) { this.config = config; - - const password = process.env.QUICKBASE_CLI_PASSWORD; - this.config.password = this.config.password || password; + this.config.password = this.config.password || process.env.QUICKBASE_CLI_PASSWORD; + this.config.username = this.config.username || process.env.QUICKBASE_CLI_USERNAME; + this.config.appToken = this.config.appToken || process.env.QUICKBASE_CLI_APPTOKEN; + this.config.userToken = this.config.userToken || process.env.QUICKBASE_CLI_USERTOKEN; + this.authData = null; } + uploadPage(pageName, pageText) { const xmlData = ` - ${this.handleXMLChars(pageText)} - 1 - ${pageName} - `; + ${this._handleXMLChars(pageText)} + 1 + ${pageName} +`; + + return new Promise((resolve, reject) => { - return this.sendQbRequest('API_AddReplaceDBPage', xmlData); + this.sendQbRequest('API_AddReplaceDBPage', xmlData).then((response) => { + resolve(response) + }).catch((errorDesc, err) => { + reject(errorDesc, err) + }); + + }); } + // Private-ish - handleXMLChars(string) { + _handleXMLChars(string) { if (!string) { return; } @@ -41,31 +55,109 @@ class ApiClient { }); } - sendQbRequest(action, data, mainAPICall) { + + authenticateIfNeeded() { + + return new Promise((resolve, reject) => { + + //Decide here which type of authentication should be done + if (this.config.userToken) { + //Use usertoken + this.authData = `${this.config.userToken}`; + resolve() + } else if (this.config.username && this.config.password) { + //regenerate ticket first, then Use ticket + + const dbid = 'main'; + const action = "API_Authenticate"; + + const url = URL.parse( + `https://${this.config.realm}.quickbase.com/db/${dbid}?a=${action}` + ); + + const options = { + hostname: url.hostname, + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + 'QUICKBASE-ACTION': action + } + }; + + const postData = ` + + ${this.config.username} + ${this.config.password} + ${this.config.authenticate_hours} +`; + + const req = https.request(options, res => { + let response = ''; + + res.setEncoding('utf8'); + res.on('data', chunk => (response += chunk)); + res.on('end', () => { + const errCode = +response.match(/(.*)<\/errcode>/)[1]; + + if (errCode != 0) { + const errtext = response.match(/(.*)<\/errtext>/)[1]; + reject(errtext); + } else { + const ticket = response.match(/(.*)<\/ticket>/)[1]; + + this.authData = `${ticket}`; + if (this.config.appToken) { + this.authData += `${this.config.appToken}`; + } + + //Suggest to use ticket now, just validated + resolve(); + } + }); + }); + + req.on('error', err => reject('Could not send Authentication request', err)); + req.write(postData); + req.end(); + + } else { + //Error: not enough auth credentials + reject("There are not enough authentication credentials in the config or environment. Please setup a valid username and password.") + } + }); + }; + + + async sendQbRequest(action, data, mainAPICall) { + const dbid = mainAPICall ? 'main' : this.config.dbid; const url = URL.parse( `https://${this.config.realm}.quickbase.com/db/${dbid}?a=${action}` ); - const postData = ` - - ${this.config.username} - ${this.config.password} - 1 - ${this.config.appToken} - ${data} - - `; - const options = { - hostname: url.hostname, - path: url.pathname + url.search, - method: 'POST', - headers: { - 'Content-Type': 'application/xml', - 'QUICKBASE-ACTION': action - } - }; + + if (!this.authData) { + reject("You must call `authenticateIfNeeded()` before calling `sendQbRequest`"); + return; + } return new Promise((resolve, reject) => { + + const postData = ` + + ${this.authData} + ${data} +`; + const options = { + hostname: url.hostname, + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + 'QUICKBASE-ACTION': action + } + }; + const req = https.request(options, res => { let response = ''; res.setEncoding('utf8'); @@ -88,6 +180,7 @@ class ApiClient { req.end(); }); } + } module.exports = ApiClient; diff --git a/lib/generate-config.js b/lib/generate-config.js index f4d5ae0..5553d6b 100644 --- a/lib/generate-config.js +++ b/lib/generate-config.js @@ -6,7 +6,9 @@ let template = `module.exports = { realm: "{{realm}}", dbid: "{{dbid}}", appToken: "{{appToken}}", - appName: "{{appName}}" + userToken: "{{userToken}}", + appName: "{{appName}}", + authenticate_hours: "{{authenticate_hours}}", }`; const generateConfig = answers => { @@ -17,6 +19,26 @@ const generateConfig = answers => { /password: \"\{\{password\}\}\",/, `//leave commented out to use QUICKBASE_CLI_PASSWORD env variable\n\t//password:` ); + } else if (i == 'username' && answers[i] == '') { + template = template.replace( + /username: \"\{\{username\}\}\",/, + `//leave commented out to use QUICKBASE_CLI_USERNAME env variable\n\t//username:` + ); + } else if (i == 'appToken' && answers[i] == '') { + template = template.replace( + /appToken: \"\{\{appToken\}\}\",/, + `//leave commented out to use QUICKBASE_CLI_APPTOKEN env variable\n\t//appToken:` + ); + } else if (i == 'userToken' && answers[i] == '') { + template = template.replace( + /userToken: \"\{\{userToken\}\}\",/, + `//leave commented out to use QUICKBASE_CLI_USERTOKEN env variable\n\t//userToken:` + ); + } else if (i == 'authenticate_hours' && answers[i] == '') { + const authenticate_hours = parseInt(answers[i]) || 1 + template = template.replace( + /authenticate_hours: \"\{\{authenticate_hours\}\}\",/, 'authenticate_hours: "'+authenticate_hours+'"' + ); } else { template = template.replace(new RegExp(`{{${i}}}`, 'g'), answers[i]); }