feat(integrations): defer workspace-scoped install until OAuth succeeds

This commit is contained in:
Philipinho
2026-05-23 12:38:43 +01:00
parent 91a2abd8d3
commit b4c917ac07
5 changed files with 166 additions and 15 deletions
@@ -14,7 +14,11 @@ import {
useUpdateIntegrationSettings,
} from "../queries/integration-query";
import { Integration } from "../types/integration.types";
import { getOAuthAuthorizeUrl } from "../services/integration-service";
import {
getOAuthAuthorizeUrl,
getOAuthInstallUrl,
} from "../services/integration-service";
import { notifications } from "@mantine/notifications";
export default function Integrations() {
const { t } = useTranslation();
@@ -31,19 +35,29 @@ export default function Integrations() {
const handleInstall = useCallback(
async (type: string) => {
const definition = available?.find((d) => d.type === type);
try {
const integration = await installMutation.mutateAsync({ type });
if (definition?.oauth?.connectionScope === 'workspace') {
const { authorizationUrl } = await getOAuthAuthorizeUrl({
integrationId: integration.id,
});
// Workspace-scoped (Slack): the install row is only persisted when the
// OAuth callback succeeds. Skip the upfront install API call entirely.
if (definition?.oauth?.connectionScope === "workspace") {
try {
const { authorizationUrl } = await getOAuthInstallUrl({ type });
window.location.href = authorizationUrl;
} catch (err: any) {
notifications.show({
message:
err?.response?.data?.message ?? t("Failed to start installation"),
color: "red",
});
}
} catch (err) {
// installMutation's onError already shows a notification
return;
}
// Per-user OAuth providers (Linear, Jira, GitHub, ...): keep existing
// two-step flow — create the integration row, then individual users
// OAuth-connect from /settings/account/connections.
installMutation.mutate({ type });
},
[installMutation, available],
[installMutation, available, t],
);
const handleUninstall = useCallback(
@@ -68,6 +68,21 @@ export async function getOAuthAuthorizeUrl(data: {
return req.data;
}
/**
* For workspace-scoped providers: returns the authorize URL WITHOUT creating
* the integration row. The row is created atomically when the OAuth callback
* succeeds; a cancelled OAuth leaves no half-installed state.
*/
export async function getOAuthInstallUrl(data: {
type: string;
}): Promise<{ authorizationUrl: string }> {
const req = await api.post<{ authorizationUrl: string }>(
"/integrations/oauth/install",
data,
);
return req.data;
}
export async function disconnectIntegration(data: {
integrationId: string;
}): Promise<void> {