Compare commits

..

21 Commits

Author SHA1 Message Date
4d166402df Merge pull request #1824 from Infisical/create-pull-request/patch-1715660210
GH Action: rename new migration file timestamp
2024-05-14 00:17:34 -04:00
19edf83dbc chore: renamed new migration files to latest timestamp (gh-action) 2024-05-14 04:16:49 +00:00
8dee1f8fc7 Merge pull request #1800 from Infisical/gcp-iam-auth
GCP Native Authentication Method
2024-05-14 00:16:28 -04:00
3b23035dfb disable secret scanning 2024-05-13 23:12:36 -04:00
389d51fa5c Merge pull request #1819 from akhilmhdh/feat/hide-secret-scanner
feat: added secret-scanning disable option
2024-05-13 13:53:35 -04:00
638208e9fa update secret scanning text 2024-05-13 13:48:23 -04:00
c176d1e4f7 Merge pull request #1818 from akhilmhdh/fix/patches-v2
Improvised secret input component and fontawesome performance improvment
2024-05-13 13:42:30 -04:00
=
91a23a608e feat: added secret-scanning disable option 2024-05-13 21:55:37 +05:30
=
c6a25271dd fix: changed cross key to check for submission for save secret changes 2024-05-13 19:50:38 +05:30
=
0f5c1340d3 feat: dashboard optimized on font awesome levels using symbols technique 2024-05-13 13:40:59 +05:30
=
ecbdae110d feat: simplified secret input with auto completion 2024-05-13 13:40:59 +05:30
=
8ef727b4ec fix: resolved typo in dashboard nav header redirection 2024-05-13 13:40:59 +05:30
=
c6f24dbb5e fix: resolved unique key error secret input rendering 2024-05-13 13:40:59 +05:30
18c0d2fd6f Merge pull request #1814 from Infisical/aws-integration-patch
Allow updating tags in AWS Secret Manager integration
2024-05-12 15:03:19 -07:00
c1fb8f47bf Add UntagResource IAM policy requirement for AWS SM integration docs 2024-05-12 08:57:41 -07:00
990eddeb32 Merge pull request #1816 from akhilmhdh/fix/remove-migration-notice
fix: removed migration notice
2024-05-11 13:43:04 -04:00
=
ce01f8d099 fix: removed migration notice 2024-05-11 23:04:43 +05:30
faf6708b00 Merge pull request #1815 from akhilmhdh/fix/migration-mode-patch-v1
feat: maintaince mode enable machine identity login and renew
2024-05-11 11:26:21 -04:00
=
a58d6ebdac feat: maintaince mode enable machine identity login and renew 2024-05-11 20:54:00 +05:30
818b136836 Make app and appId optional in update integration endpoint 2024-05-10 19:17:40 -07:00
0cdade6a2d Update AWS SM integration to allow updating tags 2024-05-10 19:07:44 -07:00
26 changed files with 777 additions and 518 deletions

View File

@ -90,15 +90,17 @@ export const secretScanningServiceFactory = ({
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
await Promise.all(
repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({
organizationId: session.orgId,
installationId,
repository: { id, fullName: full_name }
})
)
);
if (!appCfg.DISABLE_SECRET_SCANNING) {
await Promise.all(
repositories.map(({ id, full_name }) =>
secretScanningQueue.startFullRepoScan({
organizationId: session.orgId,
installationId,
repository: { id, fullName: full_name }
})
)
);
}
return { installatedApp };
};
@ -151,6 +153,7 @@ export const secretScanningServiceFactory = ({
};
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
const appCfg = getConfig();
const { commits, repository, installation, pusher } = payload;
if (!commits || !repository || !installation || !pusher) {
return;
@ -161,13 +164,15 @@ export const secretScanningServiceFactory = ({
});
if (!installationLink) return;
await secretScanningQueue.startPushEventScan({
commits,
pusher: { name: pusher.name, email: pusher.email },
repository: { fullName: repository.full_name, id: repository.id },
organizationId: installationLink.orgId,
installationId: String(installation?.id)
});
if (!appCfg.DISABLE_SECRET_SCANNING) {
await secretScanningQueue.startPushEventScan({
commits,
pusher: { name: pusher.name, email: pusher.email },
repository: { fullName: repository.full_name, id: repository.id },
organizationId: installationLink.orgId,
installationId: String(installation?.id)
});
}
};
const handleRepoDeleteEvent = async (installationId: string, repositoryIds: string[]) => {

View File

@ -13,6 +13,10 @@ const zodStrBool = z
const envSchema = z
.object({
PORT: z.coerce.number().default(4000),
DISABLE_SECRET_SCANNING: z
.enum(["true", "false"])
.default("false")
.transform((el) => el === "true"),
REDIS_URL: zpStr(z.string()),
HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database connection string")).default(

View File

@ -5,8 +5,13 @@ import { getConfig } from "@app/lib/config/env";
export const maintenanceMode = fp(async (fastify) => {
fastify.addHook("onRequest", async (req) => {
const serverEnvs = getConfig();
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET" && serverEnvs.MAINTENANCE_MODE) {
throw new Error("Infisical is in maintenance mode. Please try again later.");
if (serverEnvs.MAINTENANCE_MODE) {
// skip if its universal auth login or renew
if (req.url === "/api/v1/auth/universal-auth/login" && req.method === "POST") return;
if (req.url === "/api/v1/auth/token/renew" && req.method === "POST") return;
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET") {
throw new Error("Infisical is in maintenance mode. Please try again later.");
}
}
});
});

View File

@ -158,7 +158,10 @@ export const registerRoutes = async (
keyStore
}: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory; keyStore: TKeyStoreFactory }
) => {
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
const appCfg = getConfig();
if (!appCfg.DISABLE_SECRET_SCANNING) {
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
}
// db layers
const userDAL = userDALFactory(db);

View File

@ -20,16 +20,23 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).merge(
z.object({ isMigrationModeOn: z.boolean() })
)
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
isMigrationModeOn: z.boolean(),
isSecretScanningDisabled: z.boolean()
})
})
}
},
handler: async () => {
const config = await getServerCfg();
const serverEnvs = getConfig();
return { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE } };
return {
config: {
...config,
isMigrationModeOn: serverEnvs.MAINTENANCE_MODE,
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING
}
};
}
});

View File

@ -143,8 +143,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
}),
body: z.object({
app: z.string().trim().describe(INTEGRATION.UPDATE.app),
appId: z.string().trim().describe(INTEGRATION.UPDATE.appId),
app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app),
appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId),
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
secretPath: z
.string()
@ -154,7 +154,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.describe(INTEGRATION.UPDATE.secretPath),
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment)
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),
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()
}),
response: {
200: z.object({

View File

@ -9,9 +9,12 @@
import {
CreateSecretCommand,
DescribeSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException,
SecretsManagerClient,
TagResourceCommand,
UntagResourceCommand,
UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager";
import { Octokit } from "@octokit/rest";
@ -574,6 +577,7 @@ const syncSecretsAWSSecretManager = async ({
if (awsSecretManagerSecret?.SecretString) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
}
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) {
await secretsManager.send(
new UpdateSecretCommand({
@ -582,7 +586,88 @@ const syncSecretsAWSSecretManager = async ({
})
);
}
const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined;
if (secretAWSTag && secretAWSTag.length) {
const describedSecret = await secretsManager.send(
// requires secretsmanager:DescribeSecret policy
new DescribeSecretCommand({
SecretId: integration.app as string
})
);
if (!describedSecret.Tags) return;
const integrationTagObj = secretAWSTag.reduce(
(acc, item) => {
acc[item.key] = item.value;
return acc;
},
{} as Record<string, string>
);
const awsTagObj = (describedSecret.Tags || []).reduce(
(acc, item) => {
if (item.Key && item.Value) {
acc[item.Key] = item.Value;
}
return acc;
},
{} as Record<string, string>
);
const tagsToUpdate: { Key: string; Value: string }[] = [];
const tagsToDelete: { Key: string; Value: string }[] = [];
describedSecret.Tags?.forEach((tag) => {
if (tag.Key && tag.Value) {
if (!(tag.Key in integrationTagObj)) {
// delete tag from AWS secret manager
tagsToDelete.push({
Key: tag.Key,
Value: tag.Value
});
} else if (tag.Value !== integrationTagObj[tag.Key]) {
// update tag in AWS secret manager
tagsToUpdate.push({
Key: tag.Key,
Value: integrationTagObj[tag.Key]
});
}
}
});
secretAWSTag?.forEach((tag) => {
if (!(tag.key in awsTagObj)) {
// create tag in AWS secret manager
tagsToUpdate.push({
Key: tag.key,
Value: tag.value
});
}
});
if (tagsToUpdate.length) {
await secretsManager.send(
new TagResourceCommand({
SecretId: integration.app as string,
Tags: tagsToUpdate
})
);
}
if (tagsToDelete.length) {
await secretsManager.send(
new UntagResourceCommand({
SecretId: integration.app as string,
TagKeys: tagsToDelete.map((tag) => tag.Key)
})
);
}
}
} catch (err) {
// case when AWS manager can't find the specified secret
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(
new CreateSecretCommand({

View File

@ -103,7 +103,8 @@ export const integrationServiceFactory = ({
owner,
isActive,
environment,
secretPath
secretPath,
metadata
}: TUpdateIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
@ -127,7 +128,17 @@ export const integrationServiceFactory = ({
appId,
targetEnvironment,
owner,
secretPath
secretPath,
metadata: {
...(integration.metadata as object),
...metadata
}
});
await secretQueueService.syncIntegrations({
environment: folder.environment.slug,
secretPath,
projectId: folder.projectId
});
return updatedIntegration;

View File

@ -33,13 +33,27 @@ export type TCreateIntegrationDTO = {
export type TUpdateIntegrationDTO = {
id: string;
app: string;
appId: string;
app?: string;
appId?: string;
isActive?: boolean;
secretPath: string;
targetEnvironment: string;
owner: string;
environment: string;
metadata?: {
secretPrefix?: string;
secretSuffix?: string;
secretGCPLabel?: {
labelName: string;
labelValue: string;
};
secretAWSTag?: {
key: string;
value: string;
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
};
} & Omit<TProjectPermission, "projectId">;
export type TDeleteIntegrationDTO = {

View File

@ -29,7 +29,9 @@ Prerequisites:
"secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret",
"secretsmanager:DescribeSecret", // if you need to add tags to secrets
"secretsmanager:TagResource", // if you need to add tags to secrets
"secretsmanager:UntagResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key
"kms:ListAliases", // if you need to specify the KMS key
"kms:Encrypt", // if you need to specify the KMS key

View File

@ -120,7 +120,7 @@ export default function NavHeader({
passHref
legacyBehavior
href={{
pathname: "/project/[id]/secrets/v2/[env]",
pathname: "/project/[id]/secrets/[env]",
query: { id: router.query.id, env: router.query.env }
}}
>

View File

@ -0,0 +1,19 @@
import { forwardRef, HTMLAttributes } from "react";
type Props = {
symbolName: string;
} & HTMLAttributes<HTMLDivElement>;
export const FontAwesomeSymbol = forwardRef<HTMLDivElement, Props>(
({ symbolName, ...props }, ref) => {
return (
<div ref={ref} {...props}>
<svg className="w-inherit h-inherit">
<use href={`#${symbolName}`} />
</svg>
</div>
);
}
);
FontAwesomeSymbol.displayName = "FontAwesomeSymbol";

View File

@ -0,0 +1 @@
export { FontAwesomeSymbol } from "./FontAwesomeSymbol";

View File

@ -1,17 +1,42 @@
import { TextareaHTMLAttributes, useEffect, useRef, useState } from "react";
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover";
import { twMerge } from "tailwind-merge";
import { useWorkspace } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetFoldersByEnv, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
import { useDebounce, useToggle } from "@app/hooks";
import { useGetProjectFolders, useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
import { SecretInput } from "../SecretInput";
const REGEX_UNCLOSED_SECRET_REFERENCE = /\${(?![^{}]*\})/g;
const REGEX_OPEN_SECRET_REFERENCE = /\${/g;
const getIndexOfUnclosedRefToTheLeft = (value: string, pos: number) => {
// take substring up to pos in order to consider edits for closed references
for (let i = pos; i >= 1; i -= 1) {
if (value[i] === "}") return -1;
if (value[i - 1] === "$" && value[i] === "{") {
return i;
}
}
return -1;
};
const getIndexOfUnclosedRefToTheRight = (value: string, pos: number) => {
// use it with above to identify an open ${
for (let i = pos; i < value.length; i += 1) {
if (value[i] === "}") return i - 1;
}
return -1;
};
const getClosingSymbol = (isSelectedSecret: boolean, isClosed: boolean) => {
if (!isClosed) {
return isSelectedSecret ? "}" : ".";
}
if (!isSelectedSecret) return ".";
return "";
};
const mod = (n: number, m: number) => ((n % m) + m) % m;
export enum ReferenceType {
ENVIRONMENT = "environment",
@ -19,8 +44,9 @@ export enum ReferenceType {
SECRET = "secret"
}
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
value?: string | null;
type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "value"> & {
value?: string;
onChange: (val: string) => void;
isImport?: boolean;
isVisible?: boolean;
isReadOnly?: boolean;
@ -31,339 +57,298 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
};
type ReferenceItem = {
name: string;
label: string;
type: ReferenceType;
slug?: string;
slug: string;
};
export const InfisicalSecretInput = ({
value: propValue,
containerClassName,
secretPath: propSecretPath,
environment: propEnvironment,
onChange,
...props
}: Props) => {
const [inputValue, setInputValue] = useState(propValue ?? "");
const [isSuggestionsOpen, setIsSuggestionsOpen] = useState(false);
const [currentCursorPosition, setCurrentCursorPosition] = useState(0);
const [currentReference, setCurrentReference] = useState<string>("");
const [secretPath, setSecretPath] = useState<string>(propSecretPath || "/");
const [environment, setEnvironment] = useState<string | undefined>(propEnvironment);
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const { data: secrets } = useGetProjectSecrets({
decryptFileKey: decryptFileKey!,
environment: environment || currentWorkspace?.environments?.[0].slug!,
secretPath,
workspaceId
});
const { folderNames: folders } = useGetFoldersByEnv({
path: secretPath,
environments: [environment || currentWorkspace?.environments?.[0].slug!],
projectId: workspaceId
});
export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
(
{
value = "",
onChange,
containerClassName,
secretPath: propSecretPath,
environment: propEnvironment,
...props
},
ref
) => {
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const debouncedCurrentReference = useDebounce(currentReference, 100);
const debouncedValue = useDebounce(value, 500);
const [listReference, setListReference] = useState<ReferenceItem[]>([]);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const inputRef = useRef<HTMLTextAreaElement>(null);
const isPopupOpen = isSuggestionsOpen && listReference.length > 0 && currentReference.length > 0;
const [highlightedIndex, setHighlightedIndex] = useState(-1);
useEffect(() => {
setInputValue(propValue ?? "");
}, [propValue]);
const inputRef = useRef<HTMLTextAreaElement>(null);
const popoverContentRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useToggle(false);
const currentCursorPosition = inputRef.current?.selectionStart || 0;
useEffect(() => {
let currentEnvironment = propEnvironment;
let currentSecretPath = propSecretPath || "/";
const suggestionSource = useMemo(() => {
const left = getIndexOfUnclosedRefToTheLeft(debouncedValue, currentCursorPosition - 1);
if (left === -1) return { left, value: "", predicate: "", isDeep: false };
if (!currentReference) {
setSecretPath(currentSecretPath);
setEnvironment(currentEnvironment);
return;
}
const suggestionSourceValue = debouncedValue.slice(left + 1, currentCursorPosition);
let suggestionSourceEnv: string | undefined = propEnvironment;
let suggestionSourceSecretPath: string | undefined = propSecretPath || "/";
const isNested = currentReference.includes(".");
// means its like <environment>.<folder1>.<...more folder>.secret
const isDeep = suggestionSourceValue.includes(".");
let predicate = suggestionSourceValue;
if (isDeep) {
const [envSlug, ...folderPaths] = suggestionSourceValue.split(".");
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
suggestionSourceEnv = isValidEnvSlug ? envSlug : undefined;
suggestionSourceSecretPath = `/${folderPaths.slice(0, -1)?.join("/")}`;
predicate = folderPaths[folderPaths.length - 1];
}
if (isNested) {
const [envSlug, ...folderPaths] = currentReference.split(".");
const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
currentEnvironment = isValidEnvSlug ? envSlug : undefined;
return {
left: left + 1,
// the full value inside a ${<value>}
value: suggestionSourceValue,
// the final part after staging.dev.<folder1>.<predicate>
predicate,
isOpen: left !== -1,
isDeep,
environment: suggestionSourceEnv,
secretPath: suggestionSourceSecretPath
};
}, [debouncedValue]);
// should be based on the last valid section (with .)
folderPaths.pop();
currentSecretPath = `/${folderPaths?.join("/")}`;
}
setSecretPath(currentSecretPath);
setEnvironment(currentEnvironment);
}, [debouncedCurrentReference]);
useEffect(() => {
const currentListReference: ReferenceItem[] = [];
const isNested = currentReference?.includes(".");
if (!currentReference) {
setListReference(currentListReference);
return;
}
if (!environment) {
currentWorkspace?.environments.forEach((env) => {
currentListReference.unshift({
name: env.slug,
type: ReferenceType.ENVIRONMENT
});
});
} else if (isNested) {
folders?.forEach((folder) => {
currentListReference.unshift({ name: folder, type: ReferenceType.FOLDER });
});
} else if (environment) {
currentWorkspace?.environments.forEach((env) => {
currentListReference.unshift({
name: env.slug,
type: ReferenceType.ENVIRONMENT
});
});
}
secrets?.forEach((secret) => {
currentListReference.unshift({ name: secret.key, type: ReferenceType.SECRET });
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
const { data: secrets } = useGetProjectSecrets({
decryptFileKey: decryptFileKey!,
environment: suggestionSource.environment || "",
secretPath: suggestionSource.secretPath || "",
workspaceId,
options: {
enabled: isPopupOpen
}
});
// Get fragment inside currentReference
const searchFragment = isNested ? currentReference.split(".").pop() || "" : currentReference;
const filteredListRef = currentListReference
.filter((suggestionEntry) =>
suggestionEntry.name.toUpperCase().startsWith(searchFragment.toUpperCase())
)
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
setListReference(filteredListRef);
}, [secrets, environment, debouncedCurrentReference]);
const getIndexOfUnclosedRefToTheLeft = (pos: number) => {
// take substring up to pos in order to consider edits for closed references
const unclosedReferenceIndexMatches = [
...inputValue.substring(0, pos).matchAll(REGEX_UNCLOSED_SECRET_REFERENCE)
].map((match) => match.index);
// find unclosed reference index less than the current cursor position
let indexIter = -1;
unclosedReferenceIndexMatches.forEach((index) => {
if (index !== undefined && index > indexIter && index < pos) {
indexIter = index;
const { data: folders } = useGetProjectFolders({
environment: suggestionSource.environment || "",
path: suggestionSource.secretPath || "",
projectId: workspaceId,
options: {
enabled: isPopupOpen
}
});
return indexIter;
};
const suggestions = useMemo(() => {
if (!isPopupOpen) return [];
// reset highlight whenever recomputation happens
setHighlightedIndex(-1);
const suggestionsArr: ReferenceItem[] = [];
const predicate = suggestionSource.predicate.toLowerCase();
const getIndexOfUnclosedRefToTheRight = (pos: number) => {
const unclosedReferenceIndexMatches = [...inputValue.matchAll(REGEX_OPEN_SECRET_REFERENCE)].map(
(match) => match.index
);
// find the next unclosed reference index to the right of the current cursor position
// this is so that we know the limitation for slicing references
let indexIter = Infinity;
unclosedReferenceIndexMatches.forEach((index) => {
if (index !== undefined && index > pos && index < indexIter) {
indexIter = index;
if (!suggestionSource.isDeep) {
// At first level only environments and secrets
(currentWorkspace?.environments || []).forEach(({ name, slug }) => {
if (name.toLowerCase().startsWith(predicate))
suggestionsArr.push({
label: name,
slug,
type: ReferenceType.ENVIRONMENT
});
});
} else {
// one deeper levels its based on an environment folders and secrets
(folders || []).forEach(({ name }) => {
if (name.toLowerCase().startsWith(predicate))
suggestionsArr.push({
label: name,
slug: name,
type: ReferenceType.FOLDER
});
});
}
});
(secrets || []).forEach(({ key }) => {
if (key.toLowerCase().startsWith(predicate))
suggestionsArr.push({
label: key,
slug: key,
type: ReferenceType.SECRET
});
});
return suggestionsArr;
}, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
return indexIter;
};
const handleSuggestionSelect = (selectIndex?: number) => {
const selectedSuggestion =
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
if (!selectedSuggestion) {
return;
}
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// open suggestions if current position is to the right of an unclosed secret reference
const indexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
if (indexIter === -1) {
return;
}
setIsSuggestionsOpen(true);
if (e.key !== "Enter") {
// current reference is then going to be based on the text from the closest ${ to the right
// until the current cursor position
const openReferenceValue = inputValue.slice(indexIter + 2, currentCursorPosition);
setCurrentReference(openReferenceValue);
}
};
const handleSuggestionSelect = (selectedIndex?: number) => {
const selectedSuggestion = listReference[selectedIndex ?? highlightedIndex];
if (!selectedSuggestion) {
return;
}
const leftIndexIter = getIndexOfUnclosedRefToTheLeft(currentCursorPosition);
const rightIndexLimit = getIndexOfUnclosedRefToTheRight(currentCursorPosition);
if (leftIndexIter === -1) {
return;
}
let newValue = "";
const currentOpenRef = inputValue.slice(leftIndexIter + 2, currentCursorPosition);
if (currentOpenRef.includes(".")) {
// append suggestion after last DOT (.)
const lastDotIndex = currentReference.lastIndexOf(".");
const existingPath = currentReference.slice(0, lastDotIndex);
const refEndAfterAppending = Math.min(
leftIndexIter +
3 +
existingPath.length +
selectedSuggestion.name.length +
Number(selectedSuggestion.type !== ReferenceType.SECRET),
rightIndexLimit - 1
const rightBracketIndex = getIndexOfUnclosedRefToTheRight(value, suggestionSource.left);
const isEnclosed = rightBracketIndex !== -1;
// <lhsValue>${}<rhsvalue>
const lhsValue = value.slice(0, suggestionSource.left);
const rhsValue = value.slice(
rightBracketIndex !== -1 ? rightBracketIndex + 1 : currentCursorPosition
);
// mid will be computed value inside the interpolation
const mid = suggestionSource.isDeep
? `${suggestionSource.value.slice(0, -suggestionSource.predicate.length || undefined)}${selectedSuggestion.slug
}`
: selectedSuggestion.slug;
// whether we should append . or closing bracket on selecting suggestion
const closingSymbol = getClosingSymbol(
selectedSuggestion.type === ReferenceType.SECRET,
isEnclosed
);
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${existingPath}.${
selectedSuggestion.name
}${selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"}${inputValue.slice(
refEndAfterAppending
)}`;
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
setCurrentReference(openReferenceValue);
const newValue = `${lhsValue}${mid}${closingSymbol}${rhsValue}`;
onChange?.(newValue);
// this delay is for cursor adjustment
// cannot do this without a delay because what happens in onChange gets propogated after the cursor change
// Thus the cursor goes last to avoid that we put a slight delay on cursor change to make it happen later
const delay = setTimeout(() => {
clearTimeout(delay);
if (inputRef.current)
inputRef.current.selectionEnd =
lhsValue.length +
mid.length +
closingSymbol.length +
(isEnclosed && selectedSuggestion.type === ReferenceType.SECRET ? 1 : 0); // if secret is selected the cursor should move after the closing bracket -> }
}, 10);
setHighlightedIndex(-1); // reset highlight
};
// add 1 in order to prevent referenceOpen from being triggered by handleKeyUp
setCurrentCursorPosition(refEndAfterAppending + 1);
} else {
// append selectedSuggestion at position after unclosed ${
const refEndAfterAppending = Math.min(
selectedSuggestion.name.length +
leftIndexIter +
2 +
Number(selectedSuggestion.type !== ReferenceType.SECRET),
rightIndexLimit - 1
);
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// key operation should trigger only when popup is open
if (isPopupOpen) {
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex + 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
});
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex - 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
return pos;
});
} else if (e.key === "Enter" && highlightedIndex >= 0) {
e.preventDefault();
handleSuggestionSelect();
}
if (["ArrowDown", "ArrowUp", "Tab"].includes(e.key)) {
e.preventDefault();
}
}
};
newValue = `${inputValue.slice(0, leftIndexIter + 2)}${selectedSuggestion.name}${
selectedSuggestion.type !== ReferenceType.SECRET ? "." : "}"
}${inputValue.slice(refEndAfterAppending)}`;
const handlePopUpOpen = () => {
setHighlightedIndex(-1);
};
const openReferenceValue = newValue.slice(leftIndexIter + 2, refEndAfterAppending);
setCurrentReference(openReferenceValue);
setCurrentCursorPosition(refEndAfterAppending);
}
// to handle multiple ref for single component
const handleRef = useCallback((el: HTMLTextAreaElement) => {
// @ts-expect-error this is for multiple ref single component
inputRef.current = el;
if (ref) {
if (typeof ref === "function") {
ref(el);
} else {
// eslint-disable-next-line
ref.current = el;
}
}
}, []);
onChange?.({ target: { value: newValue } } as any);
setInputValue(newValue);
setHighlightedIndex(-1);
setIsSuggestionsOpen(false);
};
return (
<Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
<Popover.Trigger asChild>
<SecretInput
{...props}
ref={handleRef}
onKeyDown={handleKeyDown}
value={value}
onFocus={() => setIsFocused.on()}
onBlur={(evt) => {
// should not on blur when its mouse down selecting a item from suggestion
if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
setIsFocused.off();
}}
onChange={(e) => onChange?.(e.target.value)}
containerClassName={containerClassName}
/>
</Popover.Trigger>
<Popover.Content
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
className="relative top-2 z-[100] max-h-64 overflow-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
style={{
width: "var(--radix-popover-trigger-width)"
}}
>
<div
className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white"
ref={popoverContentRef}
>
{suggestions.map((item, i) => {
let entryIcon;
if (item.type === ReferenceType.SECRET) {
entryIcon = faKey;
} else if (item.type === ReferenceType.ENVIRONMENT) {
entryIcon = faCircle;
} else {
entryIcon = faFolder;
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const mod = (n: number, m: number) => ((n % m) + m) % m;
if (e.key === "ArrowDown") {
setHighlightedIndex((prevIndex) => mod(prevIndex + 1, listReference.length));
} else if (e.key === "ArrowUp") {
setHighlightedIndex((prevIndex) => mod(prevIndex - 1, listReference.length));
} else if (e.key === "Enter" && highlightedIndex >= 0) {
handleSuggestionSelect();
}
if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key) && isPopupOpen) {
e.preventDefault();
}
};
const setIsOpen = (isOpen: boolean) => {
setHighlightedIndex(-1);
if (isSuggestionsOpen) {
setIsSuggestionsOpen(isOpen);
}
};
const handleSecretChange = (e: any) => {
// propagate event to react-hook-form onChange
if (onChange) {
onChange(e);
}
setCurrentCursorPosition(inputRef.current?.selectionStart || 0);
setInputValue(e.target.value);
};
return (
<Popover.Root open={isPopupOpen} onOpenChange={setIsOpen}>
<Popover.Trigger asChild>
<SecretInput
{...props}
ref={inputRef}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
value={inputValue}
onChange={handleSecretChange}
containerClassName={containerClassName}
/>
</Popover.Trigger>
<Popover.Content
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
className={twMerge(
"relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
)}
style={{
width: "var(--radix-popover-trigger-width)",
maxHeight: "var(--radix-select-content-available-height)"
}}
>
<div className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md text-white">
{listReference.map((item, i) => {
let entryIcon;
if (item.type === ReferenceType.SECRET) {
entryIcon = faKey;
} else if (item.type === ReferenceType.ENVIRONMENT) {
entryIcon = faCircle;
} else {
entryIcon = faFolder;
}
return (
<div
tabIndex={0}
role="button"
onMouseDown={(e) => {
e.preventDefault();
setHighlightedIndex(i);
handleSuggestionSelect(i);
}}
style={{ pointerEvents: "auto" }}
className="flex items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
return (
<div
className={`${
highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
tabIndex={0}
role="button"
onKeyDown={(e) => {
if (e.key === "Enter") handleSuggestionSelect(i);
}}
aria-label="suggestion-item"
onClick={(e) => {
inputRef.current?.focus();
e.preventDefault();
e.stopPropagation();
handleSuggestionSelect(i);
}}
onMouseEnter={() => setHighlightedIndex(i)}
style={{ pointerEvents: "auto" }}
className="flex items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div className="flex w-full gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon
icon={entryIcon}
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
/>
<div
className={`${highlightedIndex === i ? "bg-gray-600" : ""
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
>
<div className="flex w-full gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon
icon={entryIcon}
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
/>
</div>
<div className="text-md w-10/12 truncate text-left">{item.label}</div>
</div>
<div className="text-md w-10/12 truncate text-left">{item.name}</div>
</div>
</div>
</div>
);
})}
</div>
</Popover.Content>
</Popover.Root>
);
};
);
})}
</div>
</Popover.Content>
</Popover.Root>
);
}
);
InfisicalSecretInput.displayName = "InfisicalSecretInput";

View File

@ -0,0 +1,26 @@
import { ReactNode } from "react";
import { faWarning, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
type Props = {
icon?: IconDefinition;
title: string;
children: ReactNode;
className?: string;
};
export const NoticeBanner = ({ icon = faWarning, title, children, className }: Props) => (
<div
className={twMerge(
"flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
className
)}
>
<FontAwesomeIcon icon={icon} className="pr-6 text-4xl text-white/80" />
<div className="flex w-full flex-col text-sm">
<div className="mb-2 text-lg font-semibold">{title}</div>
<div>{children}</div>
</div>
</div>
);

View File

@ -0,0 +1 @@
export { NoticeBanner } from "./NoticeBanner";

View File

@ -41,7 +41,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
// when break is added a line break works properly
return formattedContent.concat(<br />);
return formattedContent.concat(<br key={`secret-value-${formattedContent.length + 1}`} />);
};
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
@ -90,7 +90,10 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
aria-label="secret value"
ref={ref}
className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`}
onFocus={() => setIsSecretFocused.on()}
onFocus={(evt) => {
onFocus?.(evt);
setIsSecretFocused.on();
}}
disabled={isDisabled}
spellCheck={false}
onBlur={(evt) => {

View File

@ -10,12 +10,14 @@ export * from "./Drawer";
export * from "./Dropdown";
export * from "./EmailServiceSetupModal";
export * from "./EmptyState";
export * from "./FontAwesomeSymbol";
export * from "./FormControl";
export * from "./HoverCardv2";
export * from "./IconButton";
export * from "./Input";
export * from "./Menu";
export * from "./Modal";
export * from "./NoticeBanner";
export * from "./Pagination";
export * from "./Popoverv2";
export * from "./SecretInput";

View File

@ -5,6 +5,7 @@ export type TServerConfig = {
isMigrationModeOn?: boolean;
trustSamlEmails: boolean;
trustLdapEmails: boolean;
isSecretScanningDisabled: boolean;
};
export type TCreateAdminUserDTO = {

View File

@ -21,9 +21,7 @@ import {
faNetworkWired,
faPlug,
faPlus,
faUserPlus,
faWarning,
faXmark
faUserPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
@ -56,7 +54,6 @@ import {
fetchOrgUsers,
useAddUserToWsNonE2EE,
useCreateWorkspace,
useGetUserAction,
useRegisterUserAction
} from "@app/hooks/api";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
@ -312,9 +309,8 @@ const LearningItem = ({
href={link}
>
<div
className={`${
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} mb-3 rounded-md`}
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} mb-3 rounded-md`}
>
<div
onKeyDown={() => null}
@ -325,11 +321,10 @@ const LearningItem = ({
await registerUserAction.mutateAsync(userAction);
}
}}
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
complete
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
} text-mineshaft-100 duration-200`}
} text-mineshaft-100 duration-200`}
>
<div className="mr-4 flex flex-row items-center">
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
@ -407,9 +402,8 @@ const LearningItemSquare = ({
href={link}
>
<div
className={`${
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} w-full rounded-md`}
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} w-full rounded-md`}
>
<div
onKeyDown={() => null}
@ -420,11 +414,10 @@ const LearningItemSquare = ({
await registerUserAction.mutateAsync(userAction);
}
}}
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
complete
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
} text-mineshaft-100 duration-200`}
} text-mineshaft-100 duration-200`}
>
<div className="flex w-full flex-col items-center px-6 py-4">
<div className="flex w-full flex-row items-start justify-between">
@ -438,9 +431,8 @@ const LearningItemSquare = ({
</div>
)}
<div
className={`text-right text-sm font-normal text-mineshaft-300 ${
complete ? "font-semibold text-primary" : ""
}`}
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "font-semibold text-primary" : ""
}`}
>
{complete ? "Complete!" : `About ${time}`}
</div>
@ -480,14 +472,8 @@ const OrganizationPage = withPermission(
const { currentOrg } = useOrganization();
const routerOrgId = String(router.query.id);
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
const addUsersToProject = useAddUserToWsNonE2EE();
const { data: updateClosed } = useGetUserAction("april_13_2024_db_update_closed");
const registerUserAction = useRegisterUserAction();
const closeUpdate = async () => {
await registerUserAction.mutateAsync("april_13_2024_db_update_closed");
};
const addUsersToProject = useAddUserToWsNonE2EE();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addNewWs",
@ -594,31 +580,6 @@ const OrganizationPage = withPermission(
</div>
)}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
{(window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("http://localhost:8080")) && (
<div
className={`${
!updateClosed ? "block" : "hidden"
} mb-4 flex w-full flex-row items-center rounded-md border border-primary-600 bg-primary/10 p-2 text-base text-white`}
>
<FontAwesomeIcon icon={faWarning} className="p-6 text-4xl text-primary" />
<div className="text-sm">
<span className="text-lg font-semibold">Scheduled maintenance on May 11th 2024 </span>{" "}
<br />
Infisical will undergo scheduled maintenance for approximately 2 hour on Saturday, May 11th, 11am EST. During these hours, read
operations to Infisical will continue to function normally but no resources will be editable.
No action is required on your end your applications will continue to fetch secrets.
<br />
</div>
<button
type="button"
onClick={() => closeUpdate()}
aria-label="close"
className="flex h-full items-start text-mineshaft-100 duration-200 hover:text-red-400"
>
<FontAwesomeIcon icon={faXmark} />
</button>
</div>)}
<p className="mr-4 font-semibold text-white">Projects</p>
<div className="mt-6 flex w-full flex-row">
<Input
@ -748,95 +709,94 @@ const OrganizationPage = withPermission(
new Date().getTime() - new Date(user?.createdAt).getTime() <
30 * 24 * 60 * 60 * 1000
) && (
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<LearningItemSquare
text="Watch Infisical demo"
subText="Set up Infisical in 3 min."
complete={hasUserClickedIntro}
icon={faHandPeace}
time="3 min"
userAction="intro_cta_clicked"
link="https://www.youtube.com/watch?v=PK23097-25I"
/>
{orgWorkspaces.length !== 0 && (
<>
<LearningItemSquare
text="Add your secrets"
subText="Drop a .env file or type your secrets."
complete={hasUserPushedSecrets}
icon={faPlus}
time="1 min"
userAction="first_time_secrets_pushed"
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
/>
<LearningItemSquare
text="Invite your teammates"
subText="Infisical is better used as a team."
complete={usersInOrg}
icon={faUserPlus}
time="2 min"
link={`/org/${router.query.id}/members?action=invite`}
/>
</>
)}
<div className="block xl:hidden 2xl:block">
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<LearningItemSquare
text="Join Infisical Slack"
subText="Have any questions? Ask us!"
complete={hasUserClickedSlack}
icon={faSlack}
time="1 min"
userAction="slack_cta_clicked"
link="https://infisical.com/slack"
text="Watch Infisical demo"
subText="Set up Infisical in 3 min."
complete={hasUserClickedIntro}
icon={faHandPeace}
time="3 min"
userAction="intro_cta_clicked"
link="https://www.youtube.com/watch?v=PK23097-25I"
/>
{orgWorkspaces.length !== 0 && (
<>
<LearningItemSquare
text="Add your secrets"
subText="Drop a .env file or type your secrets."
complete={hasUserPushedSecrets}
icon={faPlus}
time="1 min"
userAction="first_time_secrets_pushed"
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
/>
<LearningItemSquare
text="Invite your teammates"
subText="Infisical is better used as a team."
complete={usersInOrg}
icon={faUserPlus}
time="2 min"
link={`/org/${router.query.id}/members?action=invite`}
/>
</>
)}
<div className="block xl:hidden 2xl:block">
<LearningItemSquare
text="Join Infisical Slack"
subText="Have any questions? Ask us!"
complete={hasUserClickedSlack}
icon={faSlack}
time="1 min"
userAction="slack_cta_clicked"
link="https://infisical.com/slack"
/>
</div>
</div>
</div>
{orgWorkspaces.length !== 0 && (
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
<div className="mb-4 flex w-full flex-row items-center pr-4">
<div className="mr-4 flex w-full flex-row items-center">
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
{false && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-5 w-5 text-4xl text-green"
/>
</div>
)}
<div className="flex flex-col items-start pl-0.5">
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
<div className="text-sm font-normal">
Replace .env files with a more secure and efficient alternative.
{orgWorkspaces.length !== 0 && (
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
<div className="mb-4 flex w-full flex-row items-center pr-4">
<div className="mr-4 flex w-full flex-row items-center">
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
{false && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-5 w-5 text-4xl text-green"
/>
</div>
)}
<div className="flex flex-col items-start pl-0.5">
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
<div className="text-sm font-normal">
Replace .env files with a more secure and efficient alternative.
</div>
</div>
</div>
<div
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"
}`}
>
About 2 min
</div>
</div>
<div
className={`w-28 pr-4 text-right text-sm font-semibold ${
false && "text-green"
}`}
>
About 2 min
</div>
<TabsObject />
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
<TabsObject />
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
)}
{orgWorkspaces.length !== 0 && (
<LearningItem
text="Integrate Infisical with your infrastructure"
subText="Connect Infisical to various 3rd party services and platforms."
complete={false}
icon={faPlug}
time="15 min"
link="https://infisical.com/docs/integrations/overview"
/>
)}
</div>
)}
)}
{orgWorkspaces.length !== 0 && (
<LearningItem
text="Integrate Infisical with your infrastructure"
subText="Connect Infisical to various 3rd party services and platforms."
complete={false}
icon={faPlug}
time="15 min"
link="https://infisical.com/docs/integrations/overview"
/>
)}
</div>
)}
<Modal
isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isModalOpen) => {

View File

@ -3,8 +3,8 @@ import Head from "next/head";
import { useRouter } from "next/router";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { Button, NoticeBanner } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useServerConfig } from "@app/context";
import { withPermission } from "@app/hoc";
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
@ -17,6 +17,7 @@ const SecretScanning = withPermission(
const router = useRouter();
const queryParams = router.query;
const [integrationEnabled, setIntegrationStatus] = useState(false);
const { config } = useServerConfig();
useEffect(() => {
const linkInstallation = async () => {
@ -69,6 +70,11 @@ const SecretScanning = withPermission(
<div className="mb-6 text-lg text-mineshaft-300">
Automatically monitor your GitHub activity and prevent secret leaks
</div>
{config.isSecretScanningDisabled && (
<NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
We are working on improving the performance of secret scanning due to increased usage.
</NoticeBanner>
)}
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
<div className="flex flex-col items-start">
<div className="mb-1 flex flex-row">
@ -110,7 +116,7 @@ const SecretScanning = withPermission(
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="h-min py-2"
isDisabled={!isAllowed}
isDisabled={!isAllowed || config.isSecretScanningDisabled}
>
Integrate with GitHub
</Button>

View File

@ -45,6 +45,14 @@ html {
width: 1%;
white-space: nowrap;
}
.w-inherit {
width: inherit;
}
.h-inherit {
height: inherit;
}
}
@layer components {

View File

@ -8,6 +8,7 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FontAwesomeSymbol,
FormControl,
IconButton,
Input,
@ -19,6 +20,7 @@ import {
TextArea,
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@ -29,20 +31,6 @@ import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTag,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { memo, useEffect } from "react";
@ -50,7 +38,12 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
import {
FontAwesomeSpriteName,
formSchema,
SecretActionType,
TFormSchema
} from "./SecretListView.utils";
type Props = {
secret: DecryptedSecret;
@ -206,7 +199,6 @@ export const SecretItem = memo(
}
}}
/>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div
className={twMerge(
@ -227,9 +219,12 @@ export const SecretItem = memo(
onCheckedChange={() => onToggleSecretSelect(secret.id)}
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
/>
<FontAwesomeIcon
icon={faKey}
className={twMerge("ml-3 block group-hover:hidden", isSelected && "hidden")}
<FontAwesomeSymbol
className={twMerge(
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
isSelected && "hidden"
)}
symbolName={FontAwesomeSpriteName.SecretKey}
/>
</div>
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
@ -278,10 +273,12 @@ export const SecretItem = memo(
key="secret-value"
control={control}
render={({ field }) => (
<SecretInput
<InfisicalSecretInput
isReadOnly={isReadOnly}
key="secret-value"
isVisible={isVisible}
environment={environment}
secretPath={secretPath}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/>
@ -297,7 +294,14 @@ export const SecretItem = memo(
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isSecValueCopied ? faCheck : faCopy} />
<FontAwesomeSymbol
className="h-3.5 w-3"
symbolName={
isSecValueCopied
? FontAwesomeSpriteName.Check
: FontAwesomeSpriteName.ClipboardCopy
}
/>
</IconButton>
</Tooltip>
<DropdownMenu>
@ -318,7 +322,10 @@ export const SecretItem = memo(
isDisabled={!isAllowed}
>
<Tooltip content="Tags">
<FontAwesomeIcon icon={faTags} />
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Tags}
/>
</Tooltip>
</IconButton>
</DropdownMenuTrigger>
@ -334,7 +341,14 @@ export const SecretItem = memo(
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={`${secret.id}-${tagId}`}
icon={isTagSelected && <FontAwesomeIcon icon={faCheckCircle} />}
icon={
isTagSelected && (
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.CheckedCircle}
className="h-3 w-3"
/>
)
}
iconPos="right"
>
<div className="flex items-center">
@ -353,7 +367,12 @@ export const SecretItem = memo(
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
leftIcon={
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Tags}
className="h-3 w-3"
/>
}
onClick={onCreateTag}
>
Create a tag
@ -379,7 +398,10 @@ export const SecretItem = memo(
isOverriden && "w-5 text-primary"
)}
>
<FontAwesomeIcon icon={faCodeBranch} />
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Override}
className="h-3.5 w-3.5"
/>
</IconButton>
)}
</ProjectPermissionCan>
@ -393,6 +415,7 @@ export const SecretItem = memo(
variant="plain"
size="md"
ariaLabel="add-reminder"
onClick={() => setCreateReminderFormOpen.on()}
>
<Tooltip
content={
@ -404,9 +427,9 @@ export const SecretItem = memo(
: "Reminder"
}
>
<FontAwesomeIcon
onClick={() => setCreateReminderFormOpen.on()}
icon={faClock}
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Clock}
/>
</Tooltip>
</IconButton>
@ -430,7 +453,10 @@ export const SecretItem = memo(
isDisabled={!isAllowed}
>
<Tooltip content="Comment">
<FontAwesomeIcon icon={faComment} />
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Comment}
/>
</Tooltip>
</IconButton>
</PopoverTrigger>
@ -466,10 +492,13 @@ export const SecretItem = memo(
ariaLabel="more"
variant="plain"
size="md"
className="p-0 opacity-0 group-hover:opacity-100"
className="p-0 opacity-0 group-hover:opacity-100 h-5 w-4"
onClick={() => onDetailViewSecret(secret)}
>
<FontAwesomeIcon icon={faEllipsis} size="lg" />
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.More}
className="h-5 w-4"
/>
</IconButton>
</Tooltip>
<ProjectPermissionCan
@ -488,7 +517,10 @@ export const SecretItem = memo(
onClick={() => onDeleteSecret(secret)}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faClose} size="lg" />
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close}
className="h-5 w-4"
/>
</IconButton>
)}
</ProjectPermissionCan>
@ -516,10 +548,12 @@ export const SecretItem = memo(
{isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" />
) : (
<FontAwesomeIcon
icon={faCheck}
size="lg"
className={twMerge("text-primary", errors.key && "text-mineshaft-300")}
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Check}
className={twMerge(
"h-4 w-4 text-primary",
errors.key && "text-mineshaft-300"
)}
/>
)}
</IconButton>
@ -536,7 +570,10 @@ export const SecretItem = memo(
onClick={() => reset()}
isDisabled={isSubmitting}
>
<FontAwesomeIcon icon={faClose} size="lg" />
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close}
className="h-4 w-4 text-primary"
/>
</IconButton>
</Tooltip>
</motion.div>

View File

@ -1,4 +1,5 @@
import { useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge";
@ -17,6 +18,7 @@ import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPa
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
import { SecretItem } from "./SecretItem";
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
type Props = {
secrets?: DecryptedSecret[];
@ -89,7 +91,6 @@ export const SecretListView = ({
isVisible,
isProtectedBranch = false
}: Props) => {
const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteSecret",
@ -341,6 +342,13 @@ export const SecretListView = ({
>
{namespace}
</div>
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
<FontAwesomeIcon
icon={icon}
symbol={symbol}
key={`font-awesome-svg-spritie-${symbol}`}
/>
))}
{filteredSecrets.map((secret) => (
<SecretItem
environment={environment}

View File

@ -1,4 +1,16 @@
/* eslint-disable no-nested-ternary */
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { z } from "zod";
export enum SecretActionType {
@ -41,3 +53,31 @@ export const formSchema = z.object({
});
export type TFormSchema = z.infer<typeof formSchema>;
export enum FontAwesomeSpriteName {
SecretKey = "secret-key",
Check = "check",
ClipboardCopy = "clipboard-copy",
Tags = "secret-tags",
Clock = "reminder-clock",
Comment = "comment",
More = "more",
Override = "secret-override",
Close = "close",
CheckedCircle = "check-circle"
}
// this is an optimization technique
// https://docs.fontawesome.com/web/add-icons/svg-symbols
export const FontAwesomeSpriteSymbols = [
{ icon: faKey, symbol: FontAwesomeSpriteName.SecretKey },
{ icon: faCheck, symbol: FontAwesomeSpriteName.Check },
{ icon: faCopy, symbol: FontAwesomeSpriteName.ClipboardCopy },
{ icon: faTags, symbol: FontAwesomeSpriteName.Tags },
{ icon: faClock, symbol: FontAwesomeSpriteName.Clock },
{ icon: faComment, symbol: FontAwesomeSpriteName.Comment },
{ icon: faEllipsis, symbol: FontAwesomeSpriteName.More },
{ icon: faCodeBranch, symbol: FontAwesomeSpriteName.Override },
{ icon: faClose, symbol: FontAwesomeSpriteName.Close },
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle }
];