Merge remote-tracking branch 'origin/main' into feat/add-option-to-mask-and-protect-gitlab-secrets

This commit is contained in:
Sheen Capadngan
2024-06-10 22:41:44 +08:00
19 changed files with 177 additions and 140 deletions

View File

@@ -1,7 +1,6 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG SAML_ORG_SLUG=saml-org-slug-default
FROM node:20-alpine AS base
@@ -35,9 +34,7 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
# Build
RUN npm run build
@@ -113,9 +110,6 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
WORKDIR /

View File

@@ -676,7 +676,8 @@ export const INTEGRATION = {
kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldMaskSecrets: "The flag to determine the visibility of secrets to sync. Used by Gitlab",
shouldProtectSecrets: "The flag to determine usage rules of secrets to sync. Used by Gitlab"
shouldProtectSecrets: "The flag to determine usage rules of secrets to sync. Used by Gitlab",
shouldEnableDelete: "The flag to enable deletion of secrets"
}
},
UPDATE: {

View File

@@ -75,6 +75,7 @@ const envSchema = z
.optional()
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
), // fallback since URL_GITLAB_LOGIN has been renamed
DEFAULT_SAML_ORG_SLUG: zpStr(z.string().optional()).default(process.env.NEXT_PUBLIC_SAML_ORG_SLUG),
// integration client secrets
// heroku
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
@@ -131,7 +132,8 @@ const envSchema = z
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET)
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
}));
let envCfg: Readonly<z.infer<typeof envSchema>>;

View File

@@ -919,7 +919,8 @@ export const registerRoutes = async (
emailConfigured: z.boolean().optional(),
inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(),
secretScanningConfigured: z.boolean().optional()
secretScanningConfigured: z.boolean().optional(),
samlDefaultOrgSlug: z.string().optional()
})
}
},
@@ -932,7 +933,8 @@ export const registerRoutes = async (
emailConfigured: cfg.isSmtpConfigured,
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured,
secretScanningConfigured: cfg.isSecretScanningConfigured
secretScanningConfigured: cfg.isSecretScanningConfigured,
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
};
}
});

View File

@@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list";
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
@@ -46,38 +46,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
metadata: z
.object({
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
})
.default({})
metadata: IntegrationMetadataSchema.default({})
}),
response: {
200: z.object({
@@ -163,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
metadata: z
.object({
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
})
.optional()
metadata: IntegrationMetadataSchema.optional()
}),
response: {
200: z.object({

View File

@@ -31,6 +31,7 @@ import { logger } from "@app/lib/logger";
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
@@ -1363,38 +1364,41 @@ const syncSecretsGitHub = async ({
}
}
for await (const encryptedSecret of encryptedSecrets) {
if (
!(encryptedSecret.name in secrets) &&
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
if (metadata.shouldEnableDelete) {
for await (const encryptedSecret of encryptedSecrets) {
if (
!(encryptedSecret.name in secrets) &&
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
}
}
}
@@ -2752,6 +2756,20 @@ const syncSecretsCloudflarePages = async ({
}
}
);
const metadata = z.record(z.any()).parse(integration.metadata);
if (metadata.shouldAutoRedeploy) {
await request.post(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}/deployments`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
};
/**

View File

@@ -0,0 +1,37 @@
import { z } from "zod";
import { INTEGRATION } from "@app/lib/api-docs";
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
export const IntegrationMetadataSchema = z.object({
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
});

View File

@@ -31,6 +31,7 @@ export type TCreateIntegrationDTO = {
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
};
} & Omit<TProjectPermission, "projectId">;
@@ -56,6 +57,7 @@ export type TUpdateIntegrationDTO = {
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldEnableDelete?: boolean;
};
} & Omit<TProjectPermission, "projectId">;

View File

@@ -77,7 +77,7 @@ export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsSe
// This will switch to a seal process and HMS flow in future
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = Boolean(appCfg.ROOT_ENCRYPTION_KEY);
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");

View File

@@ -496,7 +496,6 @@ To enable auto redeployment you simply have to add the following annotation to t
```yaml
secrets.infisical.com/auto-reload: "true"
```
<Accordion title="Deployment example with auto redeploy enabled">
```yaml
apiVersion: apps/v1
@@ -527,7 +526,11 @@ spec:
- containerPort: 80
```
</Accordion>
<Info>
#### How it works
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
Then, for each deployment that has this annotation present, a rolling update will be triggered.
</Info>
## Global configuration
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.

View File

@@ -318,6 +318,11 @@ SMTP_FROM_NAME=Infisical
By default, users can only login via email/password based login method.
To login into Infisical with OAuth providers such as Google, configure the associated variables.
<ParamField query="DEFAULT_SAML_ORG_SLUG" type="string">
When set, all visits to the Infisical login page will automatically redirect users of your Infisical instance to the SAML identity provider associated with the specified organization slug.
</ParamField>
<Accordion title="Google">
Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google)
@@ -369,11 +374,6 @@ To login into Infisical with OAuth providers such as Google, configure the assoc
information.
</Accordion>
<ParamField query="NEXT_PUBLIC_SAML_ORG_SLUG" type="string">
Configure SAML organization slug to automatically redirect all users of your
Infisical instance to the identity provider.
</ParamField>
## Native secret integrations
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.

View File

@@ -4,8 +4,6 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"
scripts/set-standalone-build-telemetry.sh true

View File

@@ -75,6 +75,7 @@ export const useCreateIntegration = () => {
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
};
}) => {
const {

View File

@@ -4,4 +4,5 @@ export type ServerStatus = {
emailConfigured: boolean;
secretScanningConfigured: boolean;
redisConfigured: boolean;
samlDefaultOrgSlug: boolean
};

View File

@@ -7,7 +7,15 @@ import { createNotification } from "@app/components/notifications";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem,
Switch
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@@ -34,6 +42,7 @@ export default function CloudflarePagesIntegrationPage() {
const [targetApp, setTargetApp] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironment, setTargetEnvironment] = useState("");
const [shouldAutoRedeploy, setShouldAutoRedeploy] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -69,7 +78,10 @@ export default function CloudflarePagesIntegrationPage() {
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
secretPath
secretPath,
metadata: {
shouldAutoRedeploy
}
});
setIsLoading(false);
@@ -169,6 +181,15 @@ export default function CloudflarePagesIntegrationPage() {
))}
</Select>
</FormControl>
<div className="mb-[2.36rem] ml-1 px-6">
<Switch
id="redeploy-cloudflare-pages"
onCheckedChange={(isChecked: boolean) => setShouldAutoRedeploy(isChecked)}
isChecked={shouldAutoRedeploy}
>
Auto-redeploy service upon secret change
</Switch>
</div>
<Button
onClick={handleButtonClick}
color="mineshaft"

View File

@@ -33,6 +33,7 @@ import {
Input,
Select,
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
@@ -59,7 +60,7 @@ const schema = yup.object({
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
secretPath: yup.string().trim().required("Secrets Path is required"),
secretSuffix: yup.string().trim().optional(),
shouldEnableDelete: yup.boolean().optional(),
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
repoIds: yup.mixed().when("scope", {
@@ -98,7 +99,6 @@ type FormData = yup.InferType<typeof schema>;
export default function GitHubCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const integrationAuthId =
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
@@ -120,7 +120,8 @@ export default function GitHubCreateIntegrationPage() {
defaultValues: {
secretPath: "/",
scope: "github-repo",
repoIds: []
repoIds: [],
shouldEnableDelete: false
}
});
@@ -177,7 +178,8 @@ export default function GitHubCreateIntegrationPage() {
app: targetApp.name, // repo name
owner: targetApp.owner, // repo owner
metadata: {
secretSuffix: data.secretSuffix
secretSuffix: data.secretSuffix,
shouldEnableDelete: data.shouldEnableDelete
}
});
})
@@ -194,7 +196,8 @@ export default function GitHubCreateIntegrationPage() {
scope: data.scope,
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
metadata: {
secretSuffix: data.secretSuffix
secretSuffix: data.secretSuffix,
shouldEnableDelete: data.shouldEnableDelete
}
});
break;
@@ -211,7 +214,8 @@ export default function GitHubCreateIntegrationPage() {
owner: repoOwner,
targetEnvironmentId: data.envId,
metadata: {
secretSuffix: data.secretSuffix
secretSuffix: data.secretSuffix,
shouldEnableDelete: data.shouldEnableDelete
}
});
break;
@@ -546,6 +550,21 @@ export default function GitHubCreateIntegrationPage() {
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="ml-1 mb-5">
<Controller
control={control}
name="shouldEnableDelete"
render={({ field: { onChange, value } }) => (
<Switch
id="delete-github-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Delete secrets in Github that are not in Infisical
</Switch>
)}
/>
</div>
<Controller
control={control}
name="secretSuffix"

View File

@@ -12,6 +12,7 @@ import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api";
import { navigateUserToSelectOrg } from "../../Login.utils";
@@ -31,21 +32,15 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const [loginError, setLoginError] = useState(false);
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const { data: serverDetails } = useFetchServerStatus();
useEffect(() => {
if (
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
) {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
window.close();
}
}, []);
if (serverDetails?.samlDefaultOrgSlug){
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${serverDetails?.samlDefaultOrgSlug}${callbackPort ? `?callback_port=${callbackPort}` : ""}`
router.push(redirectUrl);
}
}, [serverDetails?.samlDefaultOrgSlug]);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

View File

@@ -454,12 +454,12 @@ export const SecretOverviewPage = () => {
const filteredSecretNames = secKeys
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const filteredFolderNames = folderNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredDynamicSecrets = dynamicSecretNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredFolderNames = folderNames
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const filteredDynamicSecrets = dynamicSecretNames
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const isTableEmpty =
!(

View File

@@ -178,7 +178,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message}
>
<SecretInput
isVisible
isVisible={false}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
/>