Skip to content

Commit 41336d2

Browse files
committed
fix: send slack redirects to react host
1 parent 32bd37f commit 41336d2

9 files changed

Lines changed: 84 additions & 10 deletions

File tree

integrations/slack-gateway/channel_settings_handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func (g *slackGateway) handleChannelSettings(w http.ResponseWriter, r *http.Requ
1212
return
1313
}
1414
if r.Method == http.MethodGet {
15-
redirectToReactRoute(
15+
g.redirectToReactRoute(
1616
w,
1717
r,
1818
reactSlackChannelSettingsPath(g.relativeGatewayPath(r.URL.Path), r.URL.Query()),

integrations/slack-gateway/gateway_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ func TestOAuthCallbackAutoSelectsSingleInstallTargetAndUpsertsRegistry(t *testin
191191
if err != nil {
192192
t.Fatalf("parse result redirect: %v", err)
193193
}
194+
if resultLocation.Scheme != "https" || resultLocation.Host != "spritz.example.test" {
195+
t.Fatalf("expected result redirect to use Spritz host, got %s", resultLocation.String())
196+
}
194197
if resultLocation.Path != "/settings/slack/install/result" {
195198
t.Fatalf("expected React result route, got %q", resultLocation.Path)
196199
}
@@ -281,6 +284,9 @@ func TestOAuthCallbackRendersInstallTargetPickerWhenMultipleTargetsAvailable(t *
281284
if err != nil {
282285
t.Fatalf("parse picker redirect: %v", err)
283286
}
287+
if redirectURL.Scheme != "https" || redirectURL.Host != "spritz.example.test" {
288+
t.Fatalf("expected picker redirect to use Spritz host, got %s", redirectURL.String())
289+
}
284290
if redirectURL.Path != "/settings/slack/install/select" {
285291
t.Fatalf("expected React picker route, got %q", redirectURL.Path)
286292
}
@@ -7600,6 +7606,28 @@ func TestSpritzWebSocketURLPreservesBasePath(t *testing.T) {
76007606
}
76017607
}
76027608

7609+
func TestReactRouteURLUsesSpritzBaseURL(t *testing.T) {
7610+
gateway := newSlackGateway(
7611+
config{SpritzBaseURL: "https://spritz.example.test/app"},
7612+
slog.New(slog.NewTextHandler(io.Discard, nil)),
7613+
)
7614+
7615+
target := gateway.reactRouteURL("/settings/slack/workspaces/test?teamId=T_workspace_1")
7616+
parsed, err := url.Parse(target)
7617+
if err != nil {
7618+
t.Fatalf("parse react route url: %v", err)
7619+
}
7620+
if parsed.Scheme != "https" || parsed.Host != "spritz.example.test" {
7621+
t.Fatalf("expected Spritz host, got %s", target)
7622+
}
7623+
if parsed.Path != "/app/settings/slack/workspaces/test" {
7624+
t.Fatalf("expected Spritz base path to be preserved, got %q", parsed.Path)
7625+
}
7626+
if parsed.Query().Get("teamId") != "T_workspace_1" {
7627+
t.Fatalf("expected query to be preserved, got %q", parsed.RawQuery)
7628+
}
7629+
}
7630+
76037631
func signSlackRequest(header http.Header, signingSecret string, body []byte, now time.Time) {
76047632
timestamp := fmt.Sprintf("%d", now.Unix())
76057633
base := "v0:" + timestamp + ":" + string(body)

integrations/slack-gateway/install_result.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ func classifyInstallUpsertError(err error) installResultCode {
422422

423423
func (g *slackGateway) handleInstallResult(w http.ResponseWriter, r *http.Request) {
424424
target := url.URL{Path: reactSlackInstallResultPath(), RawQuery: r.URL.RawQuery}
425-
redirectToReactRoute(w, r, target.String())
425+
g.redirectToReactRoute(w, r, target.String())
426426
}
427427

428428
func (g *slackGateway) renderInstallResultPage(w http.ResponseWriter, r *http.Request) {

integrations/slack-gateway/react_routes.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,28 @@ func reactSlackChannelSettingsPath(relativeGatewayPath string, query url.Values)
4545
return target.String()
4646
}
4747

48-
func redirectToReactRoute(w http.ResponseWriter, r *http.Request, target string) {
49-
http.Redirect(w, r, target, http.StatusSeeOther)
48+
func (g *slackGateway) reactRouteURL(target string) string {
49+
route, err := url.Parse(strings.TrimSpace(target))
50+
if err != nil {
51+
return target
52+
}
53+
if route.IsAbs() {
54+
return route.String()
55+
}
56+
57+
base, err := url.Parse(strings.TrimRight(strings.TrimSpace(g.cfg.SpritzBaseURL), "/"))
58+
if err != nil || base.Scheme == "" || base.Host == "" {
59+
return route.String()
60+
}
61+
basePath := strings.TrimRight(base.Path, "/")
62+
routePath := "/" + strings.TrimLeft(route.Path, "/")
63+
base.RawPath = ""
64+
base.Path = basePath + routePath
65+
base.RawQuery = route.RawQuery
66+
base.Fragment = route.Fragment
67+
return base.String()
68+
}
69+
70+
func (g *slackGateway) redirectToReactRoute(w http.ResponseWriter, r *http.Request, target string) {
71+
http.Redirect(w, r, g.reactRouteURL(target), http.StatusSeeOther)
5072
}

integrations/slack-gateway/slack_oauth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func (g *slackGateway) handleOAuthCallback(w http.ResponseWriter, r *http.Reques
216216
return
217217
}
218218
g.setPendingInstallCookie(w, r, requestID, pendingState)
219-
redirectToReactRoute(w, r, reactSlackInstallSelectPath(requestID))
219+
g.redirectToReactRoute(w, r, reactSlackInstallSelectPath(requestID))
220220
return
221221
}
222222
if err := g.upsertInstallation(r.Context(), &installation, requestID, targets[0].PresetInputs); err != nil {

integrations/slack-gateway/workspace_handlers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ func (g *slackGateway) handleWorkspaceManagement(w http.ResponseWriter, r *http.
1010
if !ok {
1111
return
1212
}
13-
redirectToReactRoute(w, r, reactSlackWorkspacesPath(r.URL.Query()))
13+
g.redirectToReactRoute(w, r, reactSlackWorkspacesPath(r.URL.Query()))
1414
_ = principal
1515
}
1616

@@ -51,7 +51,7 @@ func (g *slackGateway) handleWorkspaceTargetPicker(w http.ResponseWriter, r *htt
5151
if !ok {
5252
return
5353
}
54-
redirectToReactRoute(w, r, reactSlackWorkspaceTargetPath(r.URL.Query()))
54+
g.redirectToReactRoute(w, r, reactSlackWorkspaceTargetPath(r.URL.Query()))
5555
_ = principal
5656
}
5757

integrations/slack-gateway/workspace_test_messages.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ func (g *slackGateway) handleWorkspaceTestForm(w http.ResponseWriter, r *http.Re
291291
if !ok {
292292
return
293293
}
294-
redirectToReactRoute(w, r, reactSlackWorkspaceTestPath(r.URL.Query()))
294+
g.redirectToReactRoute(w, r, reactSlackWorkspaceTestPath(r.URL.Query()))
295295
_ = principal
296296
}
297297

ui/src/pages/settings.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,28 @@ describe('SettingsPage', () => {
282282
expect(await screen.findByText('identity.unresolved')).toBeTruthy();
283283
expect(await screen.findByText('Request ID: install-request-1')).toBeTruthy();
284284
});
285+
286+
it('does not render query-provided install result action links', async () => {
287+
requestMock.mockImplementation((path: string) => {
288+
if (path.startsWith('/api/slack/install/result?')) {
289+
return Promise.reject(new Error('gateway unavailable'));
290+
}
291+
return Promise.reject(new Error(`unexpected request: ${path}`));
292+
});
293+
294+
render(
295+
<MemoryRouter
296+
initialEntries={[
297+
'/settings/slack/install/result?status=error&code=internal.error&actionLabel=Continue&actionHref=javascript:alert(1)',
298+
]}
299+
>
300+
<Routes>
301+
<Route path="settings/*" element={<SettingsPage />} />
302+
</Routes>
303+
</MemoryRouter>,
304+
);
305+
306+
expect(await screen.findByText('Slack install needs attention')).toBeTruthy();
307+
expect(screen.queryByRole('link', { name: 'Continue' })).toBeNull();
308+
});
285309
});

ui/src/pages/settings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -852,8 +852,8 @@ function installResultFromQuery(params: URLSearchParams): SlackInstallResult | n
852852
? 'The Slack workspace is connected.'
853853
: 'The Slack install flow finished without a detailed result message.'
854854
),
855-
actionLabel: params.get('actionLabel') || undefined,
856-
actionHref: params.get('actionHref') || undefined,
855+
actionLabel: undefined,
856+
actionHref: undefined,
857857
};
858858
}
859859

0 commit comments

Comments
 (0)