feat(secret-sync): Gitlab PR suggestions

This commit is contained in:
carlosmonastyrski
2025-06-23 10:52:59 -03:00
parent 27cb686216
commit c305ddd463
18 changed files with 58 additions and 57 deletions
@@ -4,7 +4,9 @@ import { AxiosError, AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
@@ -37,6 +39,14 @@ export const getGitLabConnectionListItem = () => {
}; };
}; };
export const getGitLabInstanceUrl = async (instanceUrl?: string) => {
const gitLabInstanceUrl = instanceUrl ? removeTrailingSlash(instanceUrl) : IntegrationUrls.GITLAB_URL;
await blockLocalAndPrivateIpAddresses(gitLabInstanceUrl);
return gitLabInstanceUrl;
};
export const refreshGitLabToken = async ( export const refreshGitLabToken = async (
refreshToken: string, refreshToken: string,
appId: string, appId: string,
@@ -61,16 +71,13 @@ export const refreshGitLabToken = async (
}); });
try { try {
const { data } = await request.post<GitLabOAuthTokenResponse>( const url = await getGitLabInstanceUrl(instanceUrl);
`${instanceUrl ? `${instanceUrl}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL}`, const { data } = await request.post<GitLabOAuthTokenResponse>(`${url}/oauth/token`, payload.toString(), {
payload.toString(),
{
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json" Accept: "application/json"
} }
} });
);
const expiresAt = new Date(Date.now() + data.expires_in * 1000 - 60000); const expiresAt = new Date(Date.now() + data.expires_in * 1000 - 60000);
@@ -118,17 +125,14 @@ export const exchangeGitLabOAuthCode = async (
client_secret: CLIENT_SECRET_GITLAB_LOGIN, client_secret: CLIENT_SECRET_GITLAB_LOGIN,
redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback` redirect_uri: `${SITE_URL}/integrations/gitlab/oauth2/callback`
}); });
const url = await getGitLabInstanceUrl(instanceUrl);
const response = await request.post<GitLabOAuthTokenResponse>( const response = await request.post<GitLabOAuthTokenResponse>(`${url}/oauth/token`, payload.toString(), {
instanceUrl ? `${instanceUrl}/oauth/token` : IntegrationUrls.GITLAB_TOKEN_URL,
payload.toString(),
{
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json" Accept: "application/json"
} }
} });
);
if (!response.data) { if (!response.data) {
throw new InternalServerError({ throw new InternalServerError({
@@ -169,15 +173,13 @@ export const validateGitLabConnectionCredentials = async (config: TGitLabConnect
let response: AxiosResponse<TGitLabProject[]> | null = null; let response: AxiosResponse<TGitLabProject[]> | null = null;
try { try {
response = await request.get<TGitLabProject[]>( const url = await getGitLabInstanceUrl(inputCredentials.instanceUrl);
`${inputCredentials.instanceUrl ? `${inputCredentials.instanceUrl}/api` : IntegrationUrls.GITLAB_API_URL}/v4/groups`, response = await request.get<TGitLabProject[]>(`${url}/api/v4/groups`, {
{
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
Accept: "application/json" Accept: "application/json"
} }
} });
);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
throw new BadRequestError({ throw new BadRequestError({
@@ -236,9 +238,8 @@ export const listGitLabProjects = async ({
); );
} }
const gitLabApiUrl = appConnection.credentials.instanceUrl const url = await getGitLabInstanceUrl(appConnection.credentials.instanceUrl);
? `${appConnection.credentials.instanceUrl}/api/v4` const gitLabApiUrl = `${url}/api/v4`;
: `${IntegrationUrls.GITLAB_API_URL}/v4`;
const projects: TGitLabProject[] = []; const projects: TGitLabProject[] = [];
let page = 1; let page = 1;
@@ -433,9 +434,8 @@ export const listGitLabGroups = async ({
); );
} }
const gitLabApiUrl = appConnection.credentials.instanceUrl const url = await getGitLabInstanceUrl(appConnection.credentials.instanceUrl);
? `${appConnection.credentials.instanceUrl}/api/v4` const gitLabApiUrl = `${url}/api/v4`;
: `${IntegrationUrls.GITLAB_API_URL}/v4`;
const groups: TGitLabGroup[] = []; const groups: TGitLabGroup[] = [];
let page = 1; let page = 1;
@@ -1,5 +1,6 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { GitLabConnectionMethod, refreshGitLabToken, TGitLabConnection } from "@app/services/app-connection/gitlab"; import { GitLabConnectionMethod, refreshGitLabToken, TGitLabConnection } from "@app/services/app-connection/gitlab";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
@@ -55,8 +56,9 @@ const getValidAccessToken = async (
return connection.credentials.accessToken; return connection.credentials.accessToken;
}; };
const getGitLabApiUrl = (connection: TGitLabConnection): string => { const getGitLabApiUrl = async (connection: TGitLabConnection): Promise<string> => {
const baseUrl = connection.credentials.instanceUrl || IntegrationUrls.GITLAB_API_URL; const baseUrl = connection.credentials.instanceUrl || IntegrationUrls.GITLAB_API_URL;
await blockLocalAndPrivateIpAddresses(baseUrl);
return baseUrl.includes("/api") ? baseUrl : `${baseUrl}/api`; return baseUrl.includes("/api") ? baseUrl : `${baseUrl}/api`;
}; };
@@ -76,7 +78,7 @@ const getGitLabVariables = async ({
targetEnvironment?: string; targetEnvironment?: string;
}): Promise<TGitLabVariable[]> => { }): Promise<TGitLabVariable[]> => {
try { try {
const apiUrl = getGitLabApiUrl(connection); const apiUrl = await getGitLabApiUrl(connection);
const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId); const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId);
const headers = { const headers = {
@@ -131,7 +133,7 @@ const createGitLabVariable = async ({
variable: TGitLabVariableCreate; variable: TGitLabVariableCreate;
}): Promise<void> => { }): Promise<void> => {
try { try {
const apiUrl = getGitLabApiUrl(connection); const apiUrl = await getGitLabApiUrl(connection);
const endpoint = buildVariablesEndpoint(apiUrl, projectId); const endpoint = buildVariablesEndpoint(apiUrl, projectId);
const payload = { const payload = {
@@ -177,7 +179,7 @@ const updateGitLabVariable = async ({
targetEnvironment?: string; targetEnvironment?: string;
}): Promise<void> => { }): Promise<void> => {
try { try {
const apiUrl = getGitLabApiUrl(connection); const apiUrl = await getGitLabApiUrl(connection);
const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId); const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId);
let url = `${baseEndpoint}/${encodeURIComponent(key)}`; let url = `${baseEndpoint}/${encodeURIComponent(key)}`;
@@ -224,7 +226,7 @@ const deleteGitLabVariable = async ({
targetEnvironment?: string; targetEnvironment?: string;
}): Promise<void> => { }): Promise<void> => {
try { try {
const apiUrl = getGitLabApiUrl(connection); const apiUrl = await getGitLabApiUrl(connection);
const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId); const baseEndpoint = buildVariablesEndpoint(apiUrl, projectId);
let url = `${baseEndpoint}/${encodeURIComponent(key)}`; let url = `${baseEndpoint}/${encodeURIComponent(key)}`;
@@ -65,7 +65,7 @@ const SecretProtectionOption = ({
export const GitLabSyncFields = () => { export const GitLabSyncFields = () => {
const { control, setValue } = useFormContext< const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.Gitlab } TSecretSyncForm & { destination: SecretSync.GitLab }
>(); >();
const connectionId = useWatch({ name: "connection.id", control }); const connectionId = useWatch({ name: "connection.id", control });
@@ -259,10 +259,10 @@ export const GitLabSyncFields = () => {
control={control} control={control}
name="destinationConfig.shouldHideSecrets" name="destinationConfig.shouldHideSecrets"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<div className='"max-h-32 opacity-100" transition-all duration-300'> <div className="max-h-32 opacity-100 transition-all duration-300">
<SecretProtectionOption <SecretProtectionOption
id="should-hide-secrets" id="should-hide-secrets"
title="Mark Infisical secrets in GitLab as 'Protected' secrets" title="Mark Infisical secrets in GitLab as 'Hidden' secrets"
tooltip="Secrets can only be marked as hidden if they are also masked." tooltip="Secrets can only be marked as hidden if they are also masked."
isEnabled={value || false} isEnabled={value || false}
onChange={onChange} onChange={onChange}
@@ -71,7 +71,7 @@ export const SecretSyncDestinationFields = () => {
return <RenderSyncFields />; return <RenderSyncFields />;
case SecretSync.Flyio: case SecretSync.Flyio:
return <FlyioSyncFields />; return <FlyioSyncFields />;
case SecretSync.Gitlab: case SecretSync.GitLab:
return <GitLabSyncFields />; return <GitLabSyncFields />;
default: default:
throw new Error(`Unhandled Destination Config Field: ${destination}`); throw new Error(`Unhandled Destination Config Field: ${destination}`);
@@ -55,7 +55,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Heroku: case SecretSync.Heroku:
case SecretSync.Render: case SecretSync.Render:
case SecretSync.Flyio: case SecretSync.Flyio:
case SecretSync.Gitlab: case SecretSync.GitLab:
AdditionalSyncOptionsFieldsComponent = null; AdditionalSyncOptionsFieldsComponent = null;
break; break;
default: default:
@@ -5,7 +5,7 @@ import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const GitLabSyncReviewFields = () => { export const GitLabSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Gitlab }>(); const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.GitLab }>();
const projectId = watch("destinationConfig.projectId"); const projectId = watch("destinationConfig.projectId");
const targetEnvironment = watch("destinationConfig.targetEnvironment"); const targetEnvironment = watch("destinationConfig.targetEnvironment");
const groupId = watch("destinationConfig.groupId"); const groupId = watch("destinationConfig.groupId");
@@ -117,7 +117,7 @@ export const SecretSyncReviewFields = () => {
case SecretSync.Flyio: case SecretSync.Flyio:
DestinationFieldsComponent = <FlyioSyncReviewFields />; DestinationFieldsComponent = <FlyioSyncReviewFields />;
break; break;
case SecretSync.Gitlab: case SecretSync.GitLab:
DestinationFieldsComponent = <GitLabSyncReviewFields />; DestinationFieldsComponent = <GitLabSyncReviewFields />;
break; break;
default: default:
@@ -6,7 +6,7 @@ import { GitlabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
export const GitlabSyncDestinationSchema = BaseSecretSyncSchema().merge( export const GitlabSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({ z.object({
destination: z.literal(SecretSync.Gitlab), destination: z.literal(SecretSync.GitLab),
destinationConfig: z.discriminatedUnion("scope", [ destinationConfig: z.discriminatedUnion("scope", [
z.object({ z.object({
scope: z.literal(GitlabSyncScope.Individual), scope: z.literal(GitlabSyncScope.Individual),
+1 -1
View File
@@ -86,7 +86,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" }, [AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" },
[AppConnection.Render]: { name: "Render", image: "Render.png" }, [AppConnection.Render]: { name: "Render", image: "Render.png" },
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.Gitlab]: { name: "Gitlab", image: "GitLab.png" } [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }
}; };
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
+2 -2
View File
@@ -74,7 +74,7 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
name: "Fly.io", name: "Fly.io",
image: "Flyio.svg" image: "Flyio.svg"
}, },
[SecretSync.Gitlab]: { [SecretSync.GitLab]: {
name: "Gitlab", name: "Gitlab",
image: "GitLab.png" image: "GitLab.png"
} }
@@ -101,7 +101,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Heroku]: AppConnection.Heroku, [SecretSync.Heroku]: AppConnection.Heroku,
[SecretSync.Render]: AppConnection.Render, [SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.Gitlab]: AppConnection.Gitlab [SecretSync.GitLab]: AppConnection.Gitlab
}; };
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
+1 -1
View File
@@ -19,7 +19,7 @@ export enum SecretSync {
Heroku = "heroku", Heroku = "heroku",
Render = "render", Render = "render",
Flyio = "flyio", Flyio = "flyio",
Gitlab = "gitlab" GitLab = "gitlab"
} }
export enum SecretSyncStatus { export enum SecretSyncStatus {
@@ -8,7 +8,7 @@ export enum GitlabSyncScope {
} }
export type TGitlabSync = TRootSecretSync & { export type TGitlabSync = TRootSecretSync & {
destination: SecretSync.Gitlab; destination: SecretSync.GitLab;
destinationConfig: destinationConfig:
| { | {
scope: GitlabSyncScope.Individual; scope: GitlabSyncScope.Individual;
@@ -68,7 +68,7 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <RenderSyncDestinationCol secretSync={secretSync} />; return <RenderSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Flyio: case SecretSync.Flyio:
return <FlyioSyncDestinationCol secretSync={secretSync} />; return <FlyioSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Gitlab: case SecretSync.GitLab:
return <GitLabSyncDestinationCol secretSync={secretSync} />; return <GitLabSyncDestinationCol secretSync={secretSync} />;
default: default:
throw new Error( throw new Error(
@@ -128,7 +128,7 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.appId; primaryText = destinationConfig.appId;
secondaryText = "App ID"; secondaryText = "App ID";
break; break;
case SecretSync.Gitlab: case SecretSync.GitLab:
primaryText = destinationConfig.projectName; primaryText = destinationConfig.projectName;
secondaryText = destinationConfig.projectId; secondaryText = destinationConfig.projectId;
break; break;
@@ -107,7 +107,7 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Flyio: case SecretSync.Flyio:
DestinationComponents = <FlyioSyncDestinationSection secretSync={secretSync} />; DestinationComponents = <FlyioSyncDestinationSection secretSync={secretSync} />;
break; break;
case SecretSync.Gitlab: case SecretSync.GitLab:
DestinationComponents = <GitLabSyncDestinationSection secretSync={secretSync} />; DestinationComponents = <GitLabSyncDestinationSection secretSync={secretSync} />;
break; break;
default: default:
@@ -58,7 +58,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Heroku: case SecretSync.Heroku:
case SecretSync.Render: case SecretSync.Render:
case SecretSync.Flyio: case SecretSync.Flyio:
case SecretSync.Gitlab: case SecretSync.GitLab:
AdditionalSyncOptionsComponent = null; AdditionalSyncOptionsComponent = null;
break; break;
default: default:
@@ -33,7 +33,6 @@ export const GitLabOAuthCallbackPage = () => {
localStorage.removeItem("latestCSRFToken"); localStorage.removeItem("latestCSRFToken");
// Retrieve stored form dataAdd commentMore actions
const storedFormData = localStorage.getItem("gitlabConnectionFormData"); const storedFormData = localStorage.getItem("gitlabConnectionFormData");
if (!storedFormData) { if (!storedFormData) {
console.error("No stored form data found"); console.error("No stored form data found");