Skip to content
This repository was archived by the owner on Nov 15, 2024. It is now read-only.

Commit 43ccc96

Browse files
committed
support jira integration, some refactoring of third-party auth for more reusability
1 parent f40f6d9 commit 43ccc96

14 files changed

Lines changed: 235 additions & 131 deletions

File tree

api_server/bin/api_server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const SlackConfig = require(ConfigDirectory + '/slack.js');
1919
const GithubConfig = require(ConfigDirectory + '/github.js');
2020
const AsanaConfig = require(ConfigDirectory + '/asana.js');
2121
const TrelloConfig = require(ConfigDirectory + '/trello.js');
22+
const JiraConfig = require(ConfigDirectory + '/jira.js');
2223
const LoggerConfig = require(ConfigDirectory + '/logger.js');
2324
const EmailConfig = require(ConfigDirectory + '/email.js');
2425
const AWSConfig = require(ConfigDirectory + '/aws.js');
@@ -77,6 +78,7 @@ const MyAPICluster = new ClusterWrapper(
7778
github: GithubConfig,
7879
asana: AsanaConfig,
7980
trello: TrelloConfig,
81+
jira: JiraConfig,
8082
email: EmailConfig,
8183
aws: AWSConfig,
8284
webclient: WebClientConfig,

api_server/config/api.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,9 @@ module.exports = {
4242

4343
// environment, please use this configuration value sparingly, really anything that depends
4444
// on environment should have its own environment variable instead
45-
environment: process.env.CS_API_ENV || 'prod'
45+
environment: process.env.CS_API_ENV || 'prod',
46+
47+
// callback environment, slightly different than environment, allows for callbacks through
48+
// VPN to developers' local servers
49+
callbackEnvironment: process.env.CS_API_CALLBACK_ENV || 'prod'
4650
};

api_server/config/jira.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// asana integration configuration
2+
3+
'use strict';
4+
5+
module.exports = {
6+
// consumerKey: process.env.CS_API_JIRA_CONSUMER_KEY,
7+
appClientId: process.env.CS_API_JIRA_CLIENT_ID,
8+
appClientSecret: process.env.CS_API_JIRA_CLIENT_SECRET
9+
};

api_server/modules/asana_auth/asana_auth.js

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const APIServerModule = require(process.env.CS_API_TOP + '/lib/api_server/api_server_module.js');
66
const fetch = require('node-fetch');
77
const FS = require('fs');
8+
const FormData = require('form-data');
89

910
class AsanaAuth extends APIServerModule {
1011

@@ -14,44 +15,36 @@ class AsanaAuth extends APIServerModule {
1415
};
1516
}
1617

17-
async handleAuthRedirect (options) {
18-
const { request, provider, state } = options;
19-
const { config } = request.api;
20-
const { authOrigin } = config.api;
21-
const { appClientId } = config.asana;
22-
const { response } = request;
18+
// get redirect parameters and url to use in the redirect response
19+
getRedirectData (options) {
20+
const { request, redirectUri, state } = options;
21+
const { appClientId } = request.api.config.asana;
2322
const parameters = {
2423
client_id: appClientId,
25-
redirect_uri: `${authOrigin}/provider-token/${provider}`,
24+
redirect_uri: redirectUri,
2625
response_type: 'code',
2726
state
2827
};
29-
const query = Object.keys(parameters)
30-
.map(key => `${key}=${encodeURIComponent(parameters[key])}`)
31-
.join('&');
32-
response.redirect(`https://app.asana.com/-/oauth_authorize?${query}`);
33-
request.responseHandled = true;
28+
const url = 'https://app.asana.com/-/oauth_authorize';
29+
return { url, parameters };
3430
}
3531

36-
async preProcessTokenCallback (options) {
32+
// given an auth code, exchange it for an access token
33+
async exchangeAuthCodeForToken (options) {
3734
// must exchange the provided authorization code for an access token
38-
const { request, state, provider } = options;
39-
const { config } = request.api;
40-
const { authOrigin } = config.api;
41-
const { appClientId, appClientSecret } = config.asana;
42-
const code = request.request.query.code || '';
35+
const { request, state, code, redirectUri } = options;
36+
const { appClientId, appClientSecret } = request.api.config.asana;
4337
const parameters = {
4438
grant_type: 'authorization_code',
4539
client_id: appClientId,
4640
client_secret: appClientSecret,
4741
code,
48-
redirect_uri: `${authOrigin}/provider-token/${provider}`,
42+
redirect_uri: redirectUri,
4943
state
5044
};
51-
const FormData = require('form-data');
5245
const form = new FormData();
5346
Object.keys(parameters).forEach(key => {
54-
form.append(key, parameters[key]/*encodeURIComponent(parameters[key])*/);
47+
form.append(key, parameters[key]);
5548
});
5649
const url = 'https://app.asana.com/-/oauth_token';
5750
const response = await fetch(
@@ -65,15 +58,19 @@ class AsanaAuth extends APIServerModule {
6558
return {
6659
accessToken: responseData.access_token,
6760
refreshToken: responseData.refresh_token,
61+
expiresAt: Date.now() + (59 * 60 * 1000 + 55 * 1000), // token good for one hour, we'll give a 5-second margin
6862
data: responseData.data
6963
};
7064
}
7165

66+
// get html to display once auth is complete
7267
getAfterAuthHtml () {
7368
return this.afterAuthHtml;
7469
}
7570

71+
// initialize the module
7672
initialize () {
73+
// read in the after-auth html to display once auth is complete
7774
this.afterAuthHtml = FS.readFileSync(this.path + '/afterAuth.html', { encoding: 'utf8' });
7875
}
7976
}

api_server/modules/github_auth/github_auth.js

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,30 @@ class GithubAuth extends APIServerModule {
1414
};
1515
}
1616

17-
async handleAuthRedirect (options) {
18-
const { request, provider, state } = options;
19-
const { config } = request.api;
20-
const { authOrigin } = config.api;
21-
const { appClientId } = config.github;
22-
const { response } = request;
17+
// get redirect parameters and url to use in the redirect response
18+
getRedirectData (options) {
19+
const { request, state, redirectUri } = options;
20+
const { appClientId } = request.api.config.github;
2321
const parameters = {
2422
client_id: appClientId,
25-
redirect_uri: `${authOrigin}/provider-token/${provider}`,
23+
redirect_uri: redirectUri,
2624
scope: 'repo,user',
2725
state
2826
};
29-
const query = Object.keys(parameters)
30-
.map(key => `${key}=${encodeURIComponent(parameters[key])}`)
31-
.join('&');
32-
response.redirect(`https://github.com/login/oauth/authorize?${query}`);
33-
request.responseHandled = true;
27+
const url = 'https://github.com/login/oauth/authorize';
28+
return { url, parameters };
3429
}
3530

36-
async preProcessTokenCallback (options) {
31+
// given an auth code, exchange it for an access token
32+
async exchangeAuthCodeForToken (options) {
3733
// must exchange the provided authorization code for an access token
38-
const { request, state, provider } = options;
39-
const { config } = request.api;
40-
const { authOrigin } = config.api;
41-
const { appClientId, appClientSecret } = config.github;
42-
const code = request.request.query.code || '';
34+
const { request, state, code, redirectUri } = options;
35+
const { appClientId, appClientSecret } = request.api.config.github;
4336
const parameters = {
4437
client_id: appClientId,
4538
client_secret: appClientSecret,
4639
code,
47-
redirect_uri: `${authOrigin}/provider-token/${provider}`,
40+
redirect_uri: redirectUri,
4841
state
4942
};
5043
const query = Object.keys(parameters)
@@ -62,11 +55,14 @@ class GithubAuth extends APIServerModule {
6255
return { accessToken: responseData.access_token };
6356
}
6457

58+
// get html to display once auth is complete
6559
getAfterAuthHtml () {
6660
return this.afterAuthHtml;
6761
}
6862

63+
// initialize the module
6964
initialize () {
65+
// read in the after-auth html to display once auth is complete
7066
this.afterAuthHtml = FS.readFileSync(this.path + '/afterAuth.html', { encoding: 'utf8' });
7167
}
7268
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>All set with your Jira integration!</p>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// provide service to handle jira credential authorization
2+
3+
'use strict';
4+
5+
const APIServerModule = require(process.env.CS_API_TOP + '/lib/api_server/api_server_module.js');
6+
const fetch = require('node-fetch');
7+
const FS = require('fs');
8+
9+
class JiraAuth extends APIServerModule {
10+
11+
services () {
12+
return async () => {
13+
return { jiraAuth: this };
14+
};
15+
}
16+
17+
// get redirect parameters and url to use in the redirect response
18+
getRedirectData (options) {
19+
const { request, state, redirectUri } = options;
20+
const { appClientId } = request.api.config.jira;
21+
const parameters = {
22+
audience: 'api.atlassian.com',
23+
client_id: appClientId,
24+
scope: 'read:jira-user read:jira-work write:jira-work',
25+
redirect_uri: redirectUri,
26+
response_type: 'code',
27+
prompt: 'consent',
28+
state
29+
};
30+
const url = 'https://auth.atlassian.com/authorize';
31+
return { url, parameters };
32+
}
33+
34+
// given an auth code, exchange it for an access token
35+
async exchangeAuthCodeForToken (options) {
36+
// must exchange the provided authorization code for an access token
37+
const { request, code, redirectUri } = options;
38+
const { appClientId, appClientSecret } = request.api.config.jira;
39+
const parameters = {
40+
grant_type: 'authorization_code',
41+
client_id: appClientId,
42+
client_secret: appClientSecret,
43+
code,
44+
redirect_uri: redirectUri
45+
};
46+
const url = 'https://auth.atlassian.com/oauth/token';
47+
const response = await fetch(
48+
url,
49+
{
50+
method: 'post',
51+
body: JSON.stringify(parameters),
52+
headers: {
53+
'Content-Type': 'application/json'
54+
}
55+
}
56+
);
57+
const responseData = await response.json();
58+
const token = responseData.access_token;
59+
delete responseData.access_token;
60+
const data = {
61+
accessToken: token,
62+
data: responseData
63+
};
64+
if (responseData.expires_in) {
65+
data.expiresAt = Date.now() + responseData.expires_in * 1000 - 5000;
66+
}
67+
return data;
68+
}
69+
70+
// get html to display once auth is complete
71+
getAfterAuthHtml () {
72+
return this.afterAuthHtml;
73+
}
74+
75+
// initialize the module
76+
initialize () {
77+
// read in the after-auth html to display once auth is complete
78+
this.afterAuthHtml = FS.readFileSync(this.path + '/afterAuth.html', { encoding: 'utf8' });
79+
}
80+
}
81+
82+
module.exports = JiraAuth;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
3+
module.exports = require('./jira_auth.js');

api_server/modules/trello_auth/trello_auth.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,24 @@ class TrelloAuth extends APIServerModule {
1313
};
1414
}
1515

16-
async handleAuthRedirect (options) {
17-
const { provider, state, request } = options;
18-
const { config } = request.api;
19-
const { authOrigin } = config.api;
20-
const { apiKey } = config.trello;
21-
const { response } = request;
16+
// get redirect parameters and url to use in the redirect response
17+
getRedirectData (options) {
18+
const { request, state, redirectUri } = options;
19+
const { apiKey } = request.api.config.trello;
2220
const parameters = {
2321
expiration: 'never',
2422
name: 'CodeStream',
2523
scope: 'read,write',
2624
response_type: 'token',
2725
key: apiKey,
2826
callback_method: 'fragment',
29-
return_url: `${authOrigin}/provider-token/${provider}?state=${state}`
27+
return_url: `${redirectUri}?state=${state}`
3028
};
31-
const query = Object.keys(parameters)
32-
.map(key => `${key}=${encodeURIComponent(parameters[key])}`)
33-
.join('&');
34-
response.redirect(`https://trello.com/1/authorize?${query}`);
35-
request.responseHandled = true;
29+
const url = 'https://trello.com/1/authorize';
30+
return { parameters, url };
3631
}
3732

33+
// perform pre-processing of data from the token callback, as needed
3834
async preProcessTokenCallback (options) {
3935
// special allowance for token in the fragment, which we can't access,
4036
// so send a client script that can
@@ -63,11 +59,14 @@ class TrelloAuth extends APIServerModule {
6359
return false; // indicates to stop further processing
6460
}
6561

62+
// get html to display once auth is complete
6663
getAfterAuthHtml () {
6764
return this.afterAuthHtml;
6865
}
6966

67+
// initialize the module
7068
initialize () {
69+
// read in the after-auth html to display once auth is complete
7170
this.afterAuthHtml = FS.readFileSync(this.path + '/afterAuth.html', { encoding: 'utf8' });
7271
}
7372
}

0 commit comments

Comments
 (0)