Skip to content

Commit ecc8263

Browse files
committed
Explicitly log in to QB to avoid getting locked out due to suspicious activity, plus added support for user_token authentication
1 parent 19d1bad commit ecc8263

File tree

8 files changed

+188
-36
lines changed

8 files changed

+188
-36
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ Below are the prompts (see the [Notes](#notes) below for an important advisory r
3131
```javascript
3232
{
3333
name: 'username',
34-
message: 'QuickBase username:'
34+
message: 'QuickBase username (leave blank to use the QUICKBASE_CLI_USERNAME environment variable):'
3535
},
3636
{
3737
name: 'password',
38-
message: 'QuickBase password (Leave blank to use the QUICKBASE_CLI_PASSWORD env variable):'
38+
message: 'QuickBase password (leave blank to use the QUICKBASE_CLI_PASSWORD environment variable):'
3939
},
4040
{
4141
name: 'dbid',
@@ -47,11 +47,19 @@ Below are the prompts (see the [Notes](#notes) below for an important advisory r
4747
},
4848
{
4949
name: 'appToken',
50-
message: 'QuickBase application token (if applicable):'
50+
message: 'QuickBase application token (if applicable) (leave blank to use the QUICKBASE_CLI_APPTOKEN environment variable):'
51+
},
52+
{
53+
name: 'userToken',
54+
message: 'QuickBase user token (if applicable) (leave blank to use the QUICKBASE_CLI_USERTOKEN environment variable):'
5155
},
5256
{
5357
name: 'appName',
5458
message: 'Code page prefix (leave blank to disable prefixing uploaded pages):'
59+
},
60+
{
61+
name: 'ticketExpiryHours',
62+
message: 'Ticket expiry period in hours (default is 1):'
5563
}
5664
```
5765

@@ -92,6 +100,8 @@ For now this is only a wrapper around `git clone`. After you pull down a repo yo
92100

93101
* 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.
94102

103+
* 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`).
104+
95105
* ~~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.~~
96106

97107
* I no longer work with QuickBase applications, so the cool shit I had planned won't happen unless someone submits some dope pull requests.

bin/qb-deploy.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ if (program.watch) {
4343

4444
async function qbDeploy(source) {
4545
console.log('Uploading files to QuickBase...');
46-
46+
47+
try {
48+
await api.authenticateIfNeeded();
49+
} catch(e) {
50+
console.error(e);
51+
return;
52+
}
53+
4754
const stats = await fs.statSync(source);
4855
const isFile = stats.isFile();
4956

bin/qb-init.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const QUESTIONS = [
88
{
99
type: 'input',
1010
name: 'username',
11-
message: 'QuickBase username:'
11+
message: 'QuickBase username (leave blank to use the QUICKBASE_CLI_USERNAME environment variable):'
1212
},
1313
{
1414
type: 'password',
@@ -29,13 +29,24 @@ const QUESTIONS = [
2929
{
3030
type: 'input',
3131
name: 'appToken',
32-
message: 'QuickBase application token (if applicable):'
32+
message: 'QuickBase application token (if applicable) (leave blank to use the QUICKBASE_CLI_APPTOKEN environment variable):'
33+
},
34+
{
35+
type: 'input',
36+
name: 'userToken',
37+
message: 'QuickBase user token (if applicable) (leave blank to use the QUICKBASE_CLI_USERTOKEN environment variable):'
3338
},
3439
{
3540
type: 'input',
3641
name: 'appName',
3742
message:
3843
'Code page prefix (leave blank to disable prefixing uploaded pages):'
44+
},
45+
{
46+
type: 'input',
47+
name: 'authenticate_hours',
48+
message:
49+
'Authentication expiry period in hours (default is 1):'
3950
}
4051
];
4152

demo/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
</head>
88
<body>
99
<h1>Hello, world</h1>
10-
10+
<p>This is a page, that should be deployed using quickbase-cli to quickbase, with styling and scripts properly referenced.</p>
11+
<p class="check_css">If this paragraph is bold, CSS files are properly referenced.</p>
12+
<p class="check_js">If this paragraph is bold, JS files are properly referenced.</p>
1113
<script src="static/bundle.js"></script>
1214
</body>
1315
</html>

demo/static/bundle.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
console.log('Hello from bundle.js');
2+
var elements = document.getElementsByClassName('check_js');
3+
var checkJsElement = elements[0];
4+
checkJsElement.style.fontWeight = 'bold';

demo/static/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
body {
22
font-family: sans-serif;
33
}
4+
5+
.check_css {
6+
font-weight: bold;
7+
}

lib/api.js

Lines changed: 121 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,40 @@
11
const https = require('https');
22
const URL = require('url');
33

4+
45
class ApiClient {
6+
57
constructor(config) {
68
this.config = config;
7-
8-
const password = process.env.QUICKBASE_CLI_PASSWORD;
9-
this.config.password = this.config.password || password;
9+
this.config.password = this.config.password || process.env.QUICKBASE_CLI_PASSWORD;
10+
this.config.username = this.config.username || process.env.QUICKBASE_CLI_USERNAME;
11+
this.config.appToken = this.config.appToken || process.env.QUICKBASE_CLI_APPTOKEN;
12+
this.config.userToken = this.config.userToken || process.env.QUICKBASE_CLI_USERTOKEN;
13+
this.authData = null;
1014
}
1115

16+
1217
uploadPage(pageName, pageText) {
1318
const xmlData = `
14-
<pagebody>${this.handleXMLChars(pageText)}</pagebody>
15-
<pagetype>1</pagetype>
16-
<pagename>${pageName}</pagename>
17-
`;
19+
<pagebody>${this._handleXMLChars(pageText)}</pagebody>
20+
<pagetype>1</pagetype>
21+
<pagename>${pageName}</pagename>
22+
`;
23+
24+
return new Promise((resolve, reject) => {
1825

19-
return this.sendQbRequest('API_AddReplaceDBPage', xmlData);
26+
this.sendQbRequest('API_AddReplaceDBPage', xmlData).then((response) => {
27+
resolve(response)
28+
}).catch((errorDesc, err) => {
29+
reject(errorDesc, err)
30+
});
31+
32+
});
2033
}
2134

35+
2236
// Private-ish
23-
handleXMLChars(string) {
37+
_handleXMLChars(string) {
2438
if (!string) {
2539
return;
2640
}
@@ -41,31 +55,109 @@ class ApiClient {
4155
});
4256
}
4357

44-
sendQbRequest(action, data, mainAPICall) {
58+
59+
authenticateIfNeeded() {
60+
61+
return new Promise((resolve, reject) => {
62+
63+
//Decide here which type of authentication should be done
64+
if (this.config.userToken) {
65+
//Use usertoken
66+
this.authData = `<usertoken>${this.config.userToken}</usertoken>`;
67+
resolve()
68+
} else if (this.config.username && this.config.password) {
69+
//regenerate ticket first, then Use ticket
70+
71+
const dbid = 'main';
72+
const action = "API_Authenticate";
73+
74+
const url = URL.parse(
75+
`https://${this.config.realm}.quickbase.com/db/${dbid}?a=${action}`
76+
);
77+
78+
const options = {
79+
hostname: url.hostname,
80+
path: url.pathname + url.search,
81+
method: 'POST',
82+
headers: {
83+
'Content-Type': 'application/xml',
84+
'QUICKBASE-ACTION': action
85+
}
86+
};
87+
88+
const postData = `
89+
<qdbapi>
90+
<username>${this.config.username}</username>
91+
<password>${this.config.password}</password>
92+
<hours>${this.config.authenticate_hours}</hours>
93+
</qdbapi>`;
94+
95+
const req = https.request(options, res => {
96+
let response = '';
97+
98+
res.setEncoding('utf8');
99+
res.on('data', chunk => (response += chunk));
100+
res.on('end', () => {
101+
const errCode = +response.match(/<errcode>(.*)<\/errcode>/)[1];
102+
103+
if (errCode != 0) {
104+
const errtext = response.match(/<errtext>(.*)<\/errtext>/)[1];
105+
reject(errtext);
106+
} else {
107+
const ticket = response.match(/<ticket>(.*)<\/ticket>/)[1];
108+
109+
this.authData = `<ticket>${ticket}</ticket>`;
110+
if (this.config.appToken) {
111+
this.authData += `<apptoken>${this.config.appToken}</apptoken>`;
112+
}
113+
114+
//Suggest to use ticket now, just validated
115+
resolve();
116+
}
117+
});
118+
});
119+
120+
req.on('error', err => reject('Could not send Authentication request', err));
121+
req.write(postData);
122+
req.end();
123+
124+
} else {
125+
//Error: not enough auth credentials
126+
reject("There are not enough authentication credentials in the config or environment. Please setup a valid username and password.")
127+
}
128+
});
129+
};
130+
131+
132+
async sendQbRequest(action, data, mainAPICall) {
133+
45134
const dbid = mainAPICall ? 'main' : this.config.dbid;
46135
const url = URL.parse(
47136
`https://${this.config.realm}.quickbase.com/db/${dbid}?a=${action}`
48137
);
49-
const postData = `
50-
<qdbapi>
51-
<username>${this.config.username}</username>
52-
<password>${this.config.password}</password>
53-
<hours>1</hours>
54-
<apptoken>${this.config.appToken}</apptoken>
55-
${data}
56-
</qdbapi>
57-
`;
58-
const options = {
59-
hostname: url.hostname,
60-
path: url.pathname + url.search,
61-
method: 'POST',
62-
headers: {
63-
'Content-Type': 'application/xml',
64-
'QUICKBASE-ACTION': action
65-
}
66-
};
138+
139+
if (!this.authData) {
140+
reject("You must call `authenticateIfNeeded()` before calling `sendQbRequest`");
141+
return;
142+
}
67143

68144
return new Promise((resolve, reject) => {
145+
146+
const postData = `
147+
<qdbapi>
148+
${this.authData}
149+
${data}
150+
</qdbapi>`;
151+
const options = {
152+
hostname: url.hostname,
153+
path: url.pathname + url.search,
154+
method: 'POST',
155+
headers: {
156+
'Content-Type': 'application/xml',
157+
'QUICKBASE-ACTION': action
158+
}
159+
};
160+
69161
const req = https.request(options, res => {
70162
let response = '';
71163
res.setEncoding('utf8');
@@ -88,6 +180,7 @@ class ApiClient {
88180
req.end();
89181
});
90182
}
183+
91184
}
92185

93186
module.exports = ApiClient;

lib/generate-config.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ let template = `module.exports = {
66
realm: "{{realm}}",
77
dbid: "{{dbid}}",
88
appToken: "{{appToken}}",
9-
appName: "{{appName}}"
9+
userToken: "{{userToken}}",
10+
appName: "{{appName}}",
11+
authenticate_hours: "{{authenticate_hours}}",
1012
}`;
1113

1214
const generateConfig = answers => {
@@ -17,6 +19,26 @@ const generateConfig = answers => {
1719
/password: \"\{\{password\}\}\",/,
1820
`//leave commented out to use QUICKBASE_CLI_PASSWORD env variable\n\t//password:`
1921
);
22+
} else if (i == 'username' && answers[i] == '') {
23+
template = template.replace(
24+
/username: \"\{\{username\}\}\",/,
25+
`//leave commented out to use QUICKBASE_CLI_USERNAME env variable\n\t//username:`
26+
);
27+
} else if (i == 'appToken' && answers[i] == '') {
28+
template = template.replace(
29+
/appToken: \"\{\{appToken\}\}\",/,
30+
`//leave commented out to use QUICKBASE_CLI_APPTOKEN env variable\n\t//appToken:`
31+
);
32+
} else if (i == 'userToken' && answers[i] == '') {
33+
template = template.replace(
34+
/userToken: \"\{\{userToken\}\}\",/,
35+
`//leave commented out to use QUICKBASE_CLI_USERTOKEN env variable\n\t//userToken:`
36+
);
37+
} else if (i == 'authenticate_hours' && answers[i] == '') {
38+
const authenticate_hours = parseInt(answers[i]) || 1
39+
template = template.replace(
40+
/authenticate_hours: \"\{\{authenticate_hours\}\}\",/, 'authenticate_hours: "'+authenticate_hours+'"'
41+
);
2042
} else {
2143
template = template.replace(new RegExp(`{{${i}}}`, 'g'), answers[i]);
2244
}

0 commit comments

Comments
 (0)