Skip to content

Commit

Permalink
Organize the source files in the OAuth package (#1430)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch committed Feb 22, 2022
1 parent 56b7f01 commit 6f8fa31
Show file tree
Hide file tree
Showing 26 changed files with 1,564 additions and 1,139 deletions.
1 change: 1 addition & 0 deletions packages/oauth/.gitignore
Expand Up @@ -9,3 +9,4 @@ package-lock.json
/.nyc_output
/coverage
*.lcov
tmp/
10 changes: 6 additions & 4 deletions packages/oauth/package.json
Expand Up @@ -33,7 +33,7 @@
"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"lint": "eslint --ext .ts src",
"test": "npm run lint && npm run test:mocha",
"test:mocha": "nyc mocha --config .mocharc.json src/*.spec.js",
"test:mocha": "nyc mocha --config .mocharc.json src/*.spec.js src/**/*.spec.js src/*.spec.ts src/**/*.spec.ts",
"coverage": "codecov -F oauthhelper --root=$PWD",
"ref-docs:model": "api-extractor run",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
Expand All @@ -47,8 +47,10 @@
"lodash.isstring": "^4.0.1"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.19.4",
"@types/chai": "^4.2.11",
"@microsoft/api-extractor": "^7.3.4",
"@types/mocha": "^9.1.0",
"@types/sinon": "^10.0.11",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.0",
"chai": "^4.2.0",
Expand All @@ -59,9 +61,9 @@
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsdoc": "^30.6.1",
"eslint-plugin-node": "^11.1.0",
"mocha": "^9.1.0",
"mocha": "^9.2.1",
"nop": "^1.0.0",
"nyc": "^14.1.1",
"nyc": "^15.1.0",
"rewiremock": "^3.13.9",
"shx": "^0.3.2",
"sinon": "^9.0.2",
Expand Down
15 changes: 15 additions & 0 deletions packages/oauth/src/authorize-result.ts
@@ -0,0 +1,15 @@
// This is intentionally structurally identical to AuthorizeResult from App
// It is redefined so that this class remains loosely coupled to the rest
// of Bolt.
export interface AuthorizeResult {
botToken?: string;
botRefreshToken?: string;
botTokenExpiresAt?: number; // utc, seconds
userToken?: string;
userRefreshToken?: string;
userTokenExpiresAt?: number; // utc, seconds
botId?: string;
botUserId?: string;
teamId?: string;
enterpriseId?: string;
}
55 changes: 55 additions & 0 deletions packages/oauth/src/callback-options.spec.ts
@@ -0,0 +1,55 @@
import { assert } from 'chai';
import { describe, it } from 'mocha';
import sinon from 'sinon';
import { IncomingMessage, ServerResponse } from 'http';

import { CallbackOptions } from './callback-options';
import { MissingStateError } from './errors';

describe('CallbackOptions', async () => {
it('should have success and failure', async () => {
const callbackOptions: CallbackOptions = {
success: async (installation, options, req, resp) => {
assert.isNotNull(installation);
assert.isNotNull(options);
assert.isNotNull(req);
assert.isNotNull(resp);
},
failure: async (installation, options, req, resp) => {
assert.isNotNull(installation);
assert.isNotNull(options);
assert.isNotNull(req);
assert.isNotNull(resp);
},
};
assert.isNotNull(callbackOptions);
const installation = {
enterprise: undefined,
team: {
id: 'T111',
},
bot: {
id: 'B111',
userId: 'W111',
scopes: ['commands'],
token: 'xoxb-',
},
user: {
id: 'W222',
scopes: undefined,
token: undefined,
},
};
const options = {
scopes: ['commands', 'chat:write'],
};
const req = sinon.createStubInstance(IncomingMessage) as IncomingMessage;
const resp = sinon.createStubInstance(ServerResponse) as ServerResponse;
callbackOptions.success!(installation, options, req, resp);

const error = new MissingStateError();
callbackOptions.failure!(error, options, req, resp);
});

// TODO: tests for default callbacks
});
82 changes: 82 additions & 0 deletions packages/oauth/src/callback-options.ts
@@ -0,0 +1,82 @@
import { IncomingMessage, ServerResponse } from 'http';
import { CodedError } from './errors';
import { InstallURLOptions } from './install-url-options';
import { Installation, OrgInstallation } from './installation';

export interface CallbackOptions {
// success is given control after handleCallback() has stored the
// installation. when provided, this function must complete the
// callbackRes.
success?: (
installation: Installation | OrgInstallation,
options: InstallURLOptions,
callbackReq: IncomingMessage,
callbackRes: ServerResponse,
) => void;

// failure is given control when handleCallback() fails at any point.
// when provided, this function must complete the callbackRes.
// default:
// serve a generic "Error" web page (show detailed cause in development)
failure?: (
error: CodedError,
options: InstallURLOptions,
callbackReq: IncomingMessage,
callbackRes: ServerResponse,
) => void;
}

// Default function to call when OAuth flow is successful
export function defaultCallbackSuccess(
installation: Installation,
_options: InstallURLOptions | undefined,
_req: IncomingMessage,
res: ServerResponse,
): void {
let redirectUrl: string;

if (isNotOrgInstall(installation) && installation.appId !== undefined) {
// redirect back to Slack native app
// Changes to the workspace app was installed to, to the app home
redirectUrl = `slack://app?team=${installation.team.id}&id=${installation.appId}`;
} else if (isOrgInstall(installation)) {
// redirect to Slack app management dashboard
redirectUrl = `${installation.enterpriseUrl}manage/organization/apps/profile/${installation.appId}/workspaces/add`;
} else {
// redirect back to Slack native app
// does not change the workspace the slack client was last in
redirectUrl = 'slack://open';
}
const htmlResponse = `<html>
<meta http-equiv="refresh" content="0; URL=${redirectUrl}">
<body>
<h1>Success! Redirecting to the Slack App...</h1>
<button onClick="window.location = '${redirectUrl}'">Click here to redirect</button>
</body></html>`;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(htmlResponse);
}

// Default function to call when OAuth flow is unsuccessful
export function defaultCallbackFailure(
_error: CodedError,
_options: InstallURLOptions,
_req: IncomingMessage,
res: ServerResponse,
): void {
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end('<html><body><h1>Oops, Something Went Wrong! Please Try Again or Contact the App Owner</h1></body></html>');
}

// ------------------------------------------
// Internals
// ------------------------------------------

// Type guard to narrow Installation type to OrgInstallation
function isOrgInstall(installation: Installation): installation is OrgInstallation {
return installation.isEnterpriseInstall || false;
}

function isNotOrgInstall(installation: Installation): installation is Installation<'v1' | 'v2', false> {
return !(isOrgInstall(installation));
}

0 comments on commit 6f8fa31

Please sign in to comment.