Skip to content

Commit

Permalink
Fix slackapi#982 Enable developers to customize the "/slack/install" …
Browse files Browse the repository at this point in the history
…webpage content
  • Loading branch information
seratch committed Aug 28, 2021
1 parent 926b669 commit ac6b5bb
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 8 deletions.
8 changes: 6 additions & 2 deletions src/receivers/ExpressReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { InstallProvider, CallbackOptions, InstallProviderOptions, InstallURLOpt
import App from '../App';
import { ReceiverAuthenticityError, ReceiverMultipleAckError, ReceiverInconsistentStateError } from '../errors';
import { AnyMiddlewareArgs, Receiver, ReceiverEvent } from '../types';
import renderHtmlForInstallPath from './render-html-for-install-path';
import defaultRenderHtmlForInstallPath from './render-html-for-install-path';

// Option keys for tls.createServer() and tls.createSecureContext(), exclusive of those for http.createServer()
const httpsOptionKeys = [
Expand Down Expand Up @@ -94,6 +94,7 @@ interface InstallerOptions {
metadata?: InstallURLOptions['metadata'];
installPath?: string;
directInstall?: boolean; // see https://api.slack.com/start/distributing/directory#direct_install
renderHtmlForInstallPath?: (url: string) => string;
redirectUriPath?: string;
callbackOptions?: CallbackOptions;
userScopes?: InstallURLOptions['userScopes'];
Expand Down Expand Up @@ -202,7 +203,10 @@ export default class ExpressReceiver implements Receiver {
res.redirect(url);
} else {
// The installation starts from a landing page served by this app.
res.send(renderHtmlForInstallPath(url));
const renderHtml = installerOptions.renderHtmlForInstallPath !== undefined ?
installerOptions.renderHtmlForInstallPath :
defaultRenderHtmlForInstallPath;
res.send(renderHtml(url));
}
} catch (error) {
next(error);
Expand Down
46 changes: 45 additions & 1 deletion src/receivers/HTTPReceiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,51 @@ describe('HTTPReceiver', function () {
assert(installProviderStub.generateInstallUrl.calledWith(sinon.match({ metadata, scopes, userScopes })));
assert.isTrue(writeHead.calledWith(200));
});
it('should redirect installers if directInstallEnabled is true', async function () {
it('should use a custom HTML renderer for the install path webpage', async function () {
// Arrange
const installProviderStub = sinon.createStubInstance(InstallProvider);
const overrides = mergeOverrides(
withHttpCreateServer(this.fakeCreateServer),
withHttpsCreateServer(sinon.fake.throws('Should not be used.')),
);
const HTTPReceiver = await importHTTPReceiver(overrides);

const metadata = 'this is bat country';
const scopes = ['channels:read'];
const userScopes = ['chat:write'];
const receiver = new HTTPReceiver({
logger: noopLogger,
clientId: 'my-clientId',
clientSecret: 'my-client-secret',
signingSecret: 'secret',
stateSecret: 'state-secret',
scopes,
installerOptions: {
authVersion: 'v2',
installPath: '/hiya',
renderHtmlForInstallPath: (_) => 'Hello world!',
metadata,
userScopes,
},
});
assert.isNotNull(receiver);
receiver.installer = installProviderStub as unknown as InstallProvider;
const fakeReq: IncomingMessage = sinon.createStubInstance(IncomingMessage) as IncomingMessage;
fakeReq.url = '/hiya';
fakeReq.headers = { host: 'localhost' };
fakeReq.method = 'GET';
const fakeRes: ServerResponse = sinon.createStubInstance(ServerResponse) as unknown as ServerResponse;
const writeHead = sinon.fake();
const end = sinon.fake();
fakeRes.writeHead = writeHead;
fakeRes.end = end;
/* eslint-disable-next-line @typescript-eslint/await-thenable */
await receiver.requestListener(fakeReq, fakeRes);
assert(installProviderStub.generateInstallUrl.calledWith(sinon.match({ metadata, scopes, userScopes })));
assert.isTrue(writeHead.calledWith(200));
assert.isTrue(end.calledWith('Hello world!'));
});
it('should rediect installers if directInstall is true', async function () {
// Arrange
const installProviderStub = sinon.createStubInstance(InstallProvider);
const overrides = mergeOverrides(
Expand Down
10 changes: 8 additions & 2 deletions src/receivers/HTTPReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { URL } from 'url';
import { verify as verifySlackAuthenticity, BufferedIncomingMessage } from './verify-request';
import App from '../App';
import { Receiver, ReceiverEvent } from '../types';
import renderHtmlForInstallPath from './render-html-for-install-path';
import defaultRenderHtmlForInstallPath from './render-html-for-install-path';
import {
ReceiverMultipleAckError,
ReceiverInconsistentStateError,
Expand Down Expand Up @@ -70,6 +70,7 @@ export interface HTTPReceiverOptions {
export interface HTTPReceiverInstallerOptions {
installPath?: string;
directInstall?: boolean; // see https://api.slack.com/start/distributing/directory#direct_install
renderHtmlForInstallPath?: (url: string) => string;
redirectUriPath?: string;
stateStore?: InstallProviderOptions['stateStore']; // default ClearStateStore
authVersion?: InstallProviderOptions['authVersion']; // default 'v2'
Expand Down Expand Up @@ -102,6 +103,8 @@ export default class HTTPReceiver implements Receiver {

private directInstall?: boolean; // always defined when installer is defined

private renderHtmlForInstallPath: (url: string) => string;

private installRedirectUriPath?: string; // always defined when installer is defined

private installUrlOptions?: InstallURLOptions; // always defined when installer is defined
Expand Down Expand Up @@ -164,6 +167,9 @@ export default class HTTPReceiver implements Receiver {
};
this.installCallbackOptions = installerOptions.callbackOptions ?? {};
}
this.renderHtmlForInstallPath = installerOptions.renderHtmlForInstallPath !== undefined ?
installerOptions.renderHtmlForInstallPath :
defaultRenderHtmlForInstallPath;

// Assign the requestListener property by binding the unboundRequestListener to this instance
this.requestListener = this.unboundRequestListener.bind(this);
Expand Down Expand Up @@ -438,7 +444,7 @@ export default class HTTPReceiver implements Receiver {
} else {
// The installation starts from a landing page served by this app.
// Generate HTML response body
const body = renderHtmlForInstallPath(url);
const body = this.renderHtmlForInstallPath(url);

// Serve a basic HTML page including the "Add to Slack" button.
// Regarding headers:
Expand Down
43 changes: 43 additions & 0 deletions src/receivers/SocketModeReceiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,49 @@ describe('SocketModeReceiver', function () {
assert(fakeRes.writeHead.calledWith(200, sinon.match.object));
assert(fakeRes.end.called);
});
it('should use a custom HTML renderer for the install path webpage', async function () {
// Arrange
const installProviderStub = sinon.createStubInstance(InstallProvider);
const overrides = mergeOverrides(
withHttpCreateServer(this.fakeCreateServer),
withHttpsCreateServer(sinon.fake.throws('Should not be used.')),
);
const SocketModeReceiver = await importSocketModeReceiver(overrides);

const metadata = 'this is bat country';
const scopes = ['channels:read'];
const userScopes = ['chat:write'];
const receiver = new SocketModeReceiver({
appToken: 'my-secret',
logger: noopLogger,
clientId: 'my-clientId',
clientSecret: 'my-client-secret',
stateSecret: 'state-secret',
scopes,
installerOptions: {
authVersion: 'v2',
installPath: '/hiya',
renderHtmlForInstallPath: (_) => 'Hello world!',
metadata,
userScopes,
},
});
assert.isNotNull(receiver);
receiver.installer = installProviderStub as unknown as InstallProvider;
const fakeReq = {
url: '/hiya',
};
const fakeRes = {
writeHead: sinon.fake(),
end: sinon.fake(),
};
/* eslint-disable-next-line @typescript-eslint/await-thenable */
await this.listener(fakeReq, fakeRes);
assert(installProviderStub.generateInstallUrl.calledWith(sinon.match({ metadata, scopes, userScopes })));
assert(fakeRes.writeHead.calledWith(200, sinon.match.object));
assert(fakeRes.end.called);
assert.isTrue(fakeRes.end.calledWith('Hello world!'));
});
it('should redirect installers if directInstall is true', async function () {
// Arrange
const installProviderStub = sinon.createStubInstance(InstallProvider);
Expand Down
8 changes: 6 additions & 2 deletions src/receivers/SocketModeReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { InstallProvider, CallbackOptions, InstallProviderOptions, InstallURLOpt
import { AppsConnectionsOpenResponse } from '@slack/web-api';
import App from '../App';
import { Receiver, ReceiverEvent } from '../types';
import renderHtmlForInstallPath from './render-html-for-install-path';
import defaultRenderHtmlForInstallPath from './render-html-for-install-path';

// TODO: we throw away the key names for endpoints, so maybe we should use this interface. is it better for migrations?
// if that's the reason, let's document that with a comment.
Expand All @@ -28,6 +28,7 @@ interface InstallerOptions {
metadata?: InstallURLOptions['metadata'];
installPath?: string;
directInstall?: boolean; // see https://api.slack.com/start/distributing/directory#direct_install
renderHtmlForInstallPath?: (url: string) => string;
redirectUriPath?: string;
callbackOptions?: CallbackOptions;
userScopes?: InstallURLOptions['userScopes'];
Expand Down Expand Up @@ -118,7 +119,10 @@ export default class SocketModeReceiver implements Receiver {
res.end('');
} else {
res.writeHead(200, {});
res.end(renderHtmlForInstallPath(url));
const renderHtml = installerOptions.renderHtmlForInstallPath !== undefined ?
installerOptions.renderHtmlForInstallPath :
defaultRenderHtmlForInstallPath;
res.end(renderHtml(url));
}
} catch (err) {
const e = err as any;
Expand Down
7 changes: 6 additions & 1 deletion src/receivers/render-html-for-install-path.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function renderHtmlForInstallPath(addToSlackUrl: string): string {
export default function defaultRenderHtmlForInstallPath(addToSlackUrl: string): string {
return `<html>
<body>
<a href=${addToSlackUrl}>
Expand All @@ -13,3 +13,8 @@ export default function renderHtmlForInstallPath(addToSlackUrl: string): string
</body>
</html>`;
}

// For backward-compatibility
export function renderHtmlForInstallPath(addToSlackUrl: string): string {
return defaultRenderHtmlForInstallPath(addToSlackUrl);
}

0 comments on commit ac6b5bb

Please sign in to comment.