diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 3f28787de..edf9b8333 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -13,7 +13,8 @@ import { revokeAccess } from '../../integrations'; import { - INTEGRATION_VERCEL_API_URL + INTEGRATION_VERCEL_API_URL, + INTEGRATION_RAILWAY_API_URL } from '../../variables'; import request from '../../config/request'; @@ -203,7 +204,8 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { } /** - * Return list of available Vercel (preview) branches + * Return list of available Vercel (preview) branches for Vercel project with + * id [appId] * @param req * @param res */ @@ -246,6 +248,73 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon }); } +/** + * Return list of available Railway environments for Railway project with + * id [appId] + * @param req + * @param res + */ +export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => { + const { integrationAuthId } = req.params; + const appId = req.query.appId as string; + + interface RailwayEnvironment { + node: { + id: string; + name: string; + isEphemeral: boolean; + } + } + + interface Environment { + environmentId: string; + name: string; + } + + let environments: Environment[] = []; + + if (appId && appId !== '') { + const query = ` + query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) { + environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) { + edges { + node { + id + name + isEphemeral + } + } + } + } + `; + + const variables = { + projectId: appId + } + + const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, { + query, + variables, + }, { + headers: { + 'Authorization': `Bearer ${req.accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + environments = edges.map((e: RailwayEnvironment) => { + return ({ + name: e.node.name, + environmentId: e.node.id + }); + }); + } + + return res.status(200).send({ + environments + }); +} + /** * Delete integration authorization with id [integrationAuthId] * @param req diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 827ce21b9..b4c627925 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -24,6 +24,7 @@ export const createIntegration = async (req: Request, res: Response) => { isActive, sourceEnvironment, targetEnvironment, + targetEnvironmentId, owner, path, region @@ -39,6 +40,7 @@ export const createIntegration = async (req: Request, res: Response) => { app, appId, targetEnvironment, + targetEnvironmentId, owner, path, region, diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index ac2b27043..99b0825c1 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -21,6 +21,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, @@ -29,11 +30,13 @@ import { INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, + INTEGRATION_RAILWAY_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, } from "../variables"; import request from '../config/request'; +import axios from "axios"; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -126,6 +129,13 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_RAILWAY: + await syncSecretsRailway({ + integration, + secrets, + accessToken + }); + break; case INTEGRATION_FLYIO: await syncSecretsFlyio({ integration, @@ -1152,6 +1162,55 @@ const syncSecretsRender = async ({ } }; +/** + * Sync/push [secrets] to Railway project with id [integration.appId] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for Railway integration + */ +const syncSecretsRailway = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + const query = ` + mutation UpsertVariables($input: VariableCollectionUpsertInput!) { + variableCollectionUpsert(input: $input) + } + `; + + const input = { + projectId: integration.appId, + environmentId: integration.targetEnvironmentId, + replace: true, + variables: secrets + }; + + await request.post(INTEGRATION_RAILWAY_API_URL, { + query, + variables: { + input, + }, + }, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Railway"); + } +} + /** * Sync/push [secrets] to Fly.io app * @param {Object} obj diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 4402bc9b5..13a095609 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -23,6 +23,7 @@ export interface IIntegration { app: string; owner: string; targetEnvironment: string; + targetEnvironmentId: string; appId: string; path: string; region: string; @@ -73,6 +74,10 @@ const integrationSchema = new Schema( type: String, default: null, }, + targetEnvironmentId: { + type: String, + default: null + }, owner: { // github-specific repo owner-login type: String, diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 42b76d88d..8a862d85a 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -30,6 +30,7 @@ router.post( // new: add new integration for integration auth body('appId').trim(), body('sourceEnvironment').trim(), body('targetEnvironment').trim(), + body('targetEnvironmentId').trim(), body('owner').trim(), body('path').trim(), body('region').trim(), diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 9d0fd3aad..e8c9e7ad5 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -111,6 +111,20 @@ router.get( integrationAuthController.getIntegrationAuthVercelBranches ); +router.get( + '/:integrationAuthId/railway/environments', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER] + }), + param('integrationAuthId').exists().isString(), + query('appId').exists().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthRailwayEnvironments +); + router.delete( '/:integrationAuthId', requireAuth({ diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index 443316c19..cabf26f9a 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -2,5 +2,6 @@ export { useGetIntegrationAuthApps, useGetIntegrationAuthById, useGetIntegrationAuthTeams, - useGetIntegrationAuthVercelBranches + useGetIntegrationAuthVercelBranches, + useGetRailwayEnvironments } from './queries'; \ No newline at end of file diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 167f39a7d..50a0618f6 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -4,6 +4,7 @@ import { apiRequest } from "@app/config/request"; import { App, + Environment, IntegrationAuth, Team } from './types'; @@ -18,7 +19,14 @@ const integrationAuthKeys = { }: { integrationAuthId: string; appId: string; - }) => [{ integrationAuthId, appId }, 'integrationAuthVercelBranches'] + }) => [{ integrationAuthId, appId }, 'integrationAuthVercelBranches'] as const, + getIntegrationAuthRailwayEnvironments: ({ + integrationAuthId, + appId + }: { + integrationAuthId: string; + appId: string; + }) => [{ integrationAuthId, appId }, 'integrationAuthRailwayEnvironments'] as const, } const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -62,6 +70,22 @@ const fetchIntegrationAuthVercelBranches = async ({ return branches; }; +const fetchIntegrationAuthRailwayEnvironments = async ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, { + params: { + appId + } + }); + + return environments; +} + export const useGetIntegrationAuthById = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), @@ -114,3 +138,23 @@ export const useGetIntegrationAuthVercelBranches = ({ enabled: true }); } + +export const useGetRailwayEnvironments = ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({ + integrationAuthId, + appId, + }), + queryFn: () => fetchIntegrationAuthRailwayEnvironments({ + integrationAuthId, + appId, + }), + enabled: true + }); +} diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 4ba19c192..d13489757 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -15,4 +15,9 @@ export type App = { export type Team = { name: string; teamId: string; +} + +export type Environment = { + name: string; + environmentId: string; } \ No newline at end of file diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts index d861223f1..393c40afd 100644 --- a/frontend/src/pages/api/integrations/createIntegration.ts +++ b/frontend/src/pages/api/integrations/createIntegration.ts @@ -7,6 +7,7 @@ interface Props { appId: string | null; sourceEnvironment: string; targetEnvironment: string | null; + targetEnvironmentId: string | null; owner: string | null; path: string | null; region: string | null; @@ -24,6 +25,7 @@ const createIntegration = ({ appId, sourceEnvironment, targetEnvironment, + targetEnvironmentId, owner, path, region @@ -40,6 +42,7 @@ const createIntegration = ({ appId, sourceEnvironment, targetEnvironment, + targetEnvironmentId, owner, path, region diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx index f58d1580d..be9e3718f 100644 --- a/frontend/src/pages/integrations/[id].tsx +++ b/frontend/src/pages/integrations/[id].tsx @@ -208,7 +208,6 @@ export default function Integrations() { link = `${window.location.origin}/integrations/travisci/authorize`; break; case 'railway': - console.log('handleUnauthorized Railway: ', integrationOption); link = `${window.location.origin}/integrations/railway/authorize`; break; default: @@ -264,7 +263,7 @@ export default function Integrations() { link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`; break; case 'railway': - console.log('handleAuthorized Railway: ', integrationAuth); + link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`; break; default: break; diff --git a/frontend/src/pages/integrations/aws-parameter-store/create.tsx b/frontend/src/pages/integrations/aws-parameter-store/create.tsx index 624f5937a..01112eeff 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/create.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/create.tsx @@ -98,6 +98,7 @@ export default function AWSParameterStoreCreateIntegrationPage() { appId: null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: null, path, region: selectedAWSRegion diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx index 804cf0504..c464564d6 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx @@ -97,6 +97,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { appId: null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: null, path: null, region: selectedAWSRegion diff --git a/frontend/src/pages/integrations/azure-key-vault/create.tsx b/frontend/src/pages/integrations/azure-key-vault/create.tsx index bfd544748..779ec1798 100644 --- a/frontend/src/pages/integrations/azure-key-vault/create.tsx +++ b/frontend/src/pages/integrations/azure-key-vault/create.tsx @@ -62,6 +62,7 @@ export default function AzureKeyVaultCreateIntegrationPage() { appId: null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: null, path: null, region: null diff --git a/frontend/src/pages/integrations/circleci/create.tsx b/frontend/src/pages/integrations/circleci/create.tsx index b753e7dd7..cf0a8db69 100644 --- a/frontend/src/pages/integrations/circleci/create.tsx +++ b/frontend/src/pages/integrations/circleci/create.tsx @@ -60,6 +60,7 @@ export default function CircleCICreateIntegrationPage() { appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: null, path: null, region: null, diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx index 05b37affd..4783c64a9 100644 --- a/frontend/src/pages/integrations/flyio/create.tsx +++ b/frontend/src/pages/integrations/flyio/create.tsx @@ -61,6 +61,7 @@ export default function FlyioCreateIntegrationPage() { appId: null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: null, path: null, region: null diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx index c4368fe4f..7a9825138 100644 --- a/frontend/src/pages/integrations/github/create.tsx +++ b/frontend/src/pages/integrations/github/create.tsx @@ -64,6 +64,7 @@ export default function GitHubCreateIntegrationPage() { appId: null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: targetApp.owner, path: null, region: null diff --git a/frontend/src/pages/integrations/gitlab/create.tsx b/frontend/src/pages/integrations/gitlab/create.tsx index f4da8c9ff..6d2e3aada 100644 --- a/frontend/src/pages/integrations/gitlab/create.tsx +++ b/frontend/src/pages/integrations/gitlab/create.tsx @@ -89,6 +89,7 @@ export default function GitLabCreateIntegrationPage() { appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: null, path: null, region: null diff --git a/frontend/src/pages/integrations/heroku/create.tsx b/frontend/src/pages/integrations/heroku/create.tsx index cae8dfe44..591b56646 100644 --- a/frontend/src/pages/integrations/heroku/create.tsx +++ b/frontend/src/pages/integrations/heroku/create.tsx @@ -60,6 +60,7 @@ export default function HerokuCreateIntegrationPage() { appId: null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment: null, + targetEnvironmentId: null, owner: null, path: null, region: null diff --git a/frontend/src/pages/integrations/netlify/create.tsx b/frontend/src/pages/integrations/netlify/create.tsx index fb33e404b..88ef46f21 100644 --- a/frontend/src/pages/integrations/netlify/create.tsx +++ b/frontend/src/pages/integrations/netlify/create.tsx @@ -69,6 +69,7 @@ export default function NetlifyCreateIntegrationPage() { appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null, sourceEnvironment: selectedSourceEnvironment, targetEnvironment, + targetEnvironmentId: null, owner: null, path: null, region: null diff --git a/frontend/src/pages/integrations/railway/create.tsx b/frontend/src/pages/integrations/railway/create.tsx index 11e948628..2e09ea262 100644 --- a/frontend/src/pages/integrations/railway/create.tsx +++ b/frontend/src/pages/integrations/railway/create.tsx @@ -11,13 +11,19 @@ import { Select, SelectItem } from '../../../components/v2'; -import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById, + useGetRailwayEnvironments +} from '../../../hooks/api/integrationAuth'; import { useGetWorkspaceById } from '../../../hooks/api/workspace'; +import createIntegration from "../../api/integrations/createIntegration"; export default function RailwayCreateIntegrationPage() { const router = useRouter(); const [targetAppId, setTargetAppId] = useState(''); + const [targetEnvironmentId, setTargetEnvironmentId] = useState(''); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -27,8 +33,12 @@ export default function RailwayCreateIntegrationPage() { const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: integrationAuthId as string ?? '' }); - - + + const { data: targetEnvironments } = useGetRailwayEnvironments({ + integrationAuthId: integrationAuthId as string ?? '', + appId: targetAppId + }); + useEffect(() => { if (workspace) { setSelectedSourceEnvironment(workspace.environments[0].slug); @@ -45,25 +55,51 @@ export default function RailwayCreateIntegrationPage() { } }, [integrationAuthApps]); + useEffect(() => { + if (targetEnvironments) { + if (targetEnvironments.length > 0) { + setTargetEnvironmentId(targetEnvironments[0].environmentId); + } else { + setTargetEnvironmentId('none'); + } + } + }, [targetEnvironments]); + const handleButtonClick = async () => { try { setIsLoading(true); if (!integrationAuth?._id) return; - // const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId))?.name; + const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId); + const targetEnvironment = targetEnvironments?.find((environment) => environment.environmentId === targetEnvironmentId); - console.log('handleButtonClick'); + if (!targetApp || !targetApp.appId || !targetEnvironment) return; + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp.name, + appId: targetApp.appId, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: targetEnvironment.name, + targetEnvironmentId: targetEnvironment.environmentId, + owner: null, + path: null, + region: null + }); setIsLoading(false); + + router.push( + `/integrations/${localStorage.getItem('projectData.id')}` + ); } catch (err) { console.error(err); } } - console.log('integrationAuthApps', integrationAuthApps); - - return workspace && selectedSourceEnvironment && integrationAuthApps ? ( + return workspace && selectedSourceEnvironment && integrationAuthApps && targetEnvironments ? (
Railway Integration @@ -84,24 +120,43 @@ export default function RailwayCreateIntegrationPage() { - setTargetAppId(val)} + className='w-full border border-mineshaft-500' + isDisabled={integrationAuthApps.length === 0} + > + {integrationAuthApps.length > 0 ? ( + integrationAuthApps.map((integrationAuthApp) => ( + + {integrationAuthApp.name} + + )) + ) : ( + + No projects found - )) - ) : ( - - No projects found - - )} - + )} + + + +