feat(secret-sync): Add Heroku Secret Sync

This commit is contained in:
carlosmonastyrski
2025-06-17 10:52:55 -03:00
parent 46f9927cf1
commit c2612f242c
86 changed files with 1772 additions and 52 deletions
@@ -0,0 +1,14 @@
import { THerokuSync } from "@app/hooks/api/secretSyncs/types/heroku-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: THerokuSync;
};
export const HerokuSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -11,6 +11,7 @@ import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
@@ -58,6 +59,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <OnePassSyncDestinationCol secretSync={secretSync} />;
case SecretSync.AzureDevOps:
return <AzureDevOpsSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Heroku:
return <HerokuSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -116,6 +116,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.devopsProjectName;
secondaryText = destinationConfig.devopsProjectId;
break;
case SecretSync.Heroku:
primaryText = destinationConfig.appName;
secondaryText = destinationConfig.app;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -0,0 +1,19 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { THerokuSync } from "@app/hooks/api/secretSyncs/types/heroku-sync";
type Props = {
secretSync: THerokuSync;
};
export const HerokuSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { app, appName }
} = secretSync;
return (
<>
<GenericFieldLabel label="App name">{appName}</GenericFieldLabel>
<GenericFieldLabel label="App Id">{app}</GenericFieldLabel>
</>
);
};
@@ -21,6 +21,7 @@ import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSec
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection";
import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection";
import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection";
import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection";
@@ -93,6 +94,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.AzureDevOps:
DestinationComponents = <AzureDevOpsSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.Heroku:
DestinationComponents = <HerokuSyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.TeamCity:
case SecretSync.OCIVault:
case SecretSync.OnePass:
case SecretSync.Heroku:
AdditionalSyncOptionsComponent = null;
break;
default:
@@ -3,43 +3,99 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { ROUTE_PATHS } from "@app/const/routes";
import { useWorkspace } from "@app/context";
import { useAuthorizeIntegration } from "@app/hooks/api";
import { useCreateAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
export const HerokuOAuthCallbackPage = () => {
const navigate = useNavigate();
const { mutateAsync } = useAuthorizeIntegration();
const { mutateAsync: createAppConnection } = useCreateAppConnection();
const { mutateAsync: updateAppConnection } = useUpdateAppConnection();
const { code, state } = useSearch({
from: ROUTE_PATHS.SecretManager.Integratons.HerokuOauthCallbackPage.id
});
const { currentWorkspace } = useWorkspace();
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem("latestCSRFToken")) return;
localStorage.removeItem("latestCSRFToken");
const integrationAuth = await mutateAsync({
workspaceId: currentWorkspace.id,
code: code as string,
integration: "heroku"
});
// Validate CSRF state token
const storedState = localStorage.getItem("latestCSRFToken");
if (state !== storedState) {
console.error("CSRF token mismatch");
navigate({
to: "/organization/app-connections",
search: { error: "invalid_state" }
});
return;
}
// Clean up CSRF token
localStorage.removeItem("latestCSRFToken");
// Retrieve stored form data
const storedFormData = localStorage.getItem("herokuConnectionFormData");
if (!storedFormData) {
console.error("No stored form data found");
navigate({
to: "/organization/app-connections",
search: { error: "missing_form_data" }
});
return;
}
const formData = JSON.parse(storedFormData);
localStorage.removeItem("herokuConnectionFormData");
// Prepare app connection data with OAuth credentials
const connectionData = {
...formData,
method: HerokuConnectionMethod.OAuth,
credentials: {
code: code as string
}
};
let appConnection;
// Create or update app connection
if (formData.isUpdate && formData.connectionId) {
appConnection = await updateAppConnection({
connectionId: formData.connectionId,
...connectionData
});
} else {
appConnection = await createAppConnection({
workspaceId: currentWorkspace.id,
...connectionData
});
}
// Navigate to success page or app connections list
navigate({
to: "/secret-manager/$projectId/integrations/heroku/create",
params: {
projectId: currentWorkspace.id
},
to: "/organization/app-connections",
search: {
integrationAuthId: integrationAuth.id
success: formData.isUpdate ? "connection_updated" : "connection_created",
connectionId: appConnection.id
}
});
} catch (err) {
console.error(err);
console.error("Error handling Heroku OAuth callback:", err);
navigate({
to: "/organization/app-connections",
search: { error: "connection_failed" }
});
}
})();
}, []);
}, [code, state, navigate, createAppConnection, updateAppConnection, currentWorkspace.id]);
return <div />;
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-primary" />
<p className="text-gray-600">Connecting to Heroku...</p>
</div>
</div>
);
};