Skip to content

Commit

Permalink
Explicitly log in to QB to avoid getting locked out due to suspicious…
Browse files Browse the repository at this point in the history
… activity, plus added support for user_token authentication
  • Loading branch information
uncleflo committed Jun 3, 2021
1 parent 19d1bad commit 3447377
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 54 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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):'
}
```

Expand Down Expand Up @@ -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.
9 changes: 8 additions & 1 deletion bin/qb-deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
15 changes: 13 additions & 2 deletions bin/qb-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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):'
}
];

Expand Down
4 changes: 3 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
</head>
<body>
<h1>Hello, world</h1>

<p>This is a page, that should be deployed using quickbase-cli to quickbase, with styling and scripts properly referenced.</p>
<p class="check_css">If this paragraph is bold, CSS files are properly referenced.</p>
<p class="check_js">If this paragraph is bold, JS files are properly referenced.</p>
<script src="static/bundle.js"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions demo/static/bundle.js
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
console.log('Hello from bundle.js');
var elements = document.getElementsByClassName('check_js');
var checkJsElement = elements[0];
checkJsElement.style.fontWeight = 'bold';
4 changes: 4 additions & 0 deletions demo/static/main.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
body {
font-family: sans-serif;
}

.check_css {
font-weight: bold;
}
186 changes: 140 additions & 46 deletions lib/api.js
Original file line number Diff line number Diff line change
@@ -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 = `
<pagebody>${this.handleXMLChars(pageText)}</pagebody>
<pagetype>1</pagetype>
<pagename>${pageName}</pagename>
`;
<pagebody>${this._handleXMLChars(pageText)}</pagebody>
<pagetype>1</pagetype>
<pagename>${pageName}</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;
}
Expand All @@ -41,53 +55,133 @@ 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 = `<usertoken>${this.config.userToken}</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 = `
<qdbapi>
<username>${this.config.username}</username>
<password>${this.config.password}</password>
<hours>${this.config.authenticate_hours}</hours>
</qdbapi>`;

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>(.*)<\/errcode>/)[1];

if (errCode != 0) {
const errtext = response.match(/<errtext>(.*)<\/errtext>/)[1];
reject(errtext);
} else {
const ticket = response.match(/<ticket>(.*)<\/ticket>/)[1];

this.authData = `<ticket>${ticket}</ticket>`;
if (this.config.appToken) {
this.authData += `<apptoken>${this.config.appToken}</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 = `
<qdbapi>
<username>${this.config.username}</username>
<password>${this.config.password}</password>
<hours>1</hours>
<apptoken>${this.config.appToken}</apptoken>
${data}
</qdbapi>
`;
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 new Promise((resolve, reject) => {
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>(.*)<\/errcode>/)[1];

if (errCode != 0) {
reject(response);
} else {
resolve(response);

if(this.authData) {
const postData = `
<qdbapi>
${this.authData}
${data}
</qdbapi>`;
const options = {
hostname: url.hostname,
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/xml',
'QUICKBASE-ACTION': action
}

resolve(response);
};

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>(.*)<\/errcode>/)[1];

if (errCode != 0) {
reject(response);
} else {
resolve(response);
}

resolve(response);
});
});
});

req.on('error', err => reject('ERROR:', err));
req.write(postData);
req.end();

req.on('error', err => reject('ERROR:', err));
req.write(postData);
req.end();
}
});
}

}

module.exports = ApiClient;
24 changes: 23 additions & 1 deletion lib/generate-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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]);
}
Expand Down

0 comments on commit 3447377

Please sign in to comment.