Skip to content

Commit 8e29a03

Browse files
committed
Config now also has ticket expiration. The app now also checks for username, apptoken and usertoken in environment variables. The app also first authenticates (if necessary) using username and password, before continuously using only a ticket for all requests. A usertoken will be used preferentially if provided in config/envvar. Apptoken will be added if provided.
1 parent 19d1bad commit 8e29a03

8 files changed

Lines changed: 227 additions & 77 deletions

File tree

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: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,33 +43,37 @@ if (program.watch) {
4343

4444
async function qbDeploy(source) {
4545
console.log('Uploading files to QuickBase...');
46-
47-
const stats = await fs.statSync(source);
48-
const isFile = stats.isFile();
49-
50-
if (isFile) {
51-
return uploadToQuickbase(source)
52-
.then(res =>
53-
console.log(`Successfully uploaded to QuickBase:\n=> ${source}`)
54-
)
55-
.catch(err => console.error(err));
56-
}
57-
58-
if (!isFile) {
59-
getFiles(source).then(files => {
60-
const uploadPromises = program.replace
61-
? files.map(file => replaceUrlsAndUpload(file, files))
62-
: files.map(file => uploadToQuickbase(file));
63-
64-
return Promise.all(uploadPromises)
46+
47+
await api.authenticateIfNeeded().then( async () => {
48+
49+
const stats = await fs.statSync(source);
50+
const isFile = stats.isFile();
51+
52+
if (isFile) {
53+
return uploadToQuickbase(source)
6554
.then(res =>
66-
console.log(
67-
`Successfully uploaded to QuickBase:\n=> ${files.join('\n=> ')}`
68-
)
55+
console.log(`Successfully uploaded to QuickBase:\n=> ${source}`)
6956
)
7057
.catch(err => console.error(err));
71-
});
72-
}
58+
}
59+
60+
if (!isFile) {
61+
getFiles(source).then(files => {
62+
const uploadPromises = program.replace
63+
? files.map(file => replaceUrlsAndUpload(file, files))
64+
: files.map(file => uploadToQuickbase(file));
65+
66+
return Promise.all(uploadPromises)
67+
.then(res =>
68+
console.log(
69+
`Successfully uploaded to QuickBase:\n=> ${files.join('\n=> ')}`
70+
)
71+
)
72+
.catch(err => console.error(err));
73+
});
74+
}
75+
76+
}).catch((errorDesc) => console.error(errorDesc));
7377
}
7478

7579
function uploadToQuickbase(file, fileContents) {

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: 140 additions & 46 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,53 +55,133 @@ 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 authData = `
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(authData);
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+
}
67142

68143
return new Promise((resolve, reject) => {
69-
const req = https.request(options, res => {
70-
let response = '';
71-
res.setEncoding('utf8');
72-
res.on('data', chunk => (response += chunk));
73-
res.on('end', () => {
74-
const errCode = +response.match(/<errcode>(.*)<\/errcode>/)[1];
75-
76-
if (errCode != 0) {
77-
reject(response);
78-
} else {
79-
resolve(response);
144+
145+
if(this.authData) {
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
80158
}
81-
82-
resolve(response);
159+
};
160+
161+
const req = https.request(options, res => {
162+
let response = '';
163+
res.setEncoding('utf8');
164+
res.on('data', chunk => (response += chunk));
165+
res.on('end', () => {
166+
const errCode = +response.match(/<errcode>(.*)<\/errcode>/)[1];
167+
168+
if (errCode != 0) {
169+
reject(response);
170+
} else {
171+
resolve(response);
172+
}
173+
174+
resolve(response);
175+
});
83176
});
84-
});
85-
86-
req.on('error', err => reject('ERROR:', err));
87-
req.write(postData);
88-
req.end();
177+
178+
req.on('error', err => reject('ERROR:', err));
179+
req.write(postData);
180+
req.end();
181+
}
89182
});
90183
}
184+
91185
}
92186

93187
module.exports = ApiClient;

0 commit comments

Comments
 (0)