mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-20 01:48:03 +00:00
Compare commits
4 Commits
misc/moved
...
misc/oidc-
Author | SHA1 | Date | |
---|---|---|---|
089a7e880b | |||
3f6a0c77f1 | |||
9e4b66e215 | |||
8a14914bc3 |
@ -5,6 +5,9 @@ export const mockSmtpServer = (): TSmtpService => {
|
|||||||
return {
|
return {
|
||||||
sendMail: async (data) => {
|
sendMail: async (data) => {
|
||||||
storage.push(data);
|
storage.push(data);
|
||||||
|
},
|
||||||
|
verify: async () => {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
infisicalSymmetricDecrypt,
|
infisicalSymmetricDecrypt,
|
||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
@ -56,7 +56,7 @@ type TOidcConfigServiceFactoryDep = {
|
|||||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
||||||
};
|
};
|
||||||
@ -223,6 +223,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
let newUser: TUsers | undefined;
|
let newUser: TUsers | undefined;
|
||||||
|
|
||||||
if (serverCfg.trustOidcEmails) {
|
if (serverCfg.trustOidcEmails) {
|
||||||
|
// we prioritize getting the most complete user to create the new alias under
|
||||||
newUser = await userDAL.findOne(
|
newUser = await userDAL.findOne(
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
@ -230,6 +231,23 @@ export const oidcConfigServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
// this fetches user entries created via invites
|
||||||
|
newUser = await userDAL.findOne(
|
||||||
|
{
|
||||||
|
username: email
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newUser && !newUser.isEmailVerified) {
|
||||||
|
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
||||||
|
newUser = await userDAL.updateById(newUser.id, {
|
||||||
|
isEmailVerified: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newUser) {
|
if (!newUser) {
|
||||||
@ -332,14 +350,20 @@ export const oidcConfigServiceFactory = ({
|
|||||||
userId: user.id
|
userId: user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
await smtpService.sendMail({
|
await smtpService
|
||||||
template: SmtpTemplates.EmailVerification,
|
.sendMail({
|
||||||
subjectLine: "Infisical confirmation code",
|
template: SmtpTemplates.EmailVerification,
|
||||||
recipients: [user.email],
|
subjectLine: "Infisical confirmation code",
|
||||||
substitutions: {
|
recipients: [user.email],
|
||||||
code: token
|
substitutions: {
|
||||||
}
|
code: token
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
throw new OidcAuthError({
|
||||||
|
message: `Error sending email confirmation code for user registration - contact the Infisical instance admin. ${err.message}`
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isUserCompleted, providerAuthToken };
|
return { isUserCompleted, providerAuthToken };
|
||||||
@ -395,6 +419,18 @@ export const oidcConfigServiceFactory = ({
|
|||||||
message: `Organization bot for organization with ID '${org.id}' not found`,
|
message: `Organization bot for organization with ID '${org.id}' not found`,
|
||||||
name: "OrgBotNotFound"
|
name: "OrgBotNotFound"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
|
if (isActive && !serverCfg.trustOidcEmails) {
|
||||||
|
const isSmtpConnected = await smtpService.verify();
|
||||||
|
if (!isSmtpConnected) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Cannot enable OIDC when there are issues with the instance's SMTP configuration. Bypass this by turning on trust for OIDC emails in the server admin console."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const key = infisicalSymmetricDecrypt({
|
const key = infisicalSymmetricDecrypt({
|
||||||
ciphertext: orgBot.encryptedSymmetricKey,
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
iv: orgBot.symmetricKeyIV,
|
iv: orgBot.symmetricKeyIV,
|
||||||
|
@ -133,3 +133,15 @@ export class ScimRequestError extends Error {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class OidcAuthError extends Error {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
error: unknown;
|
||||||
|
|
||||||
|
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
|
||||||
|
super(message || "Something went wrong");
|
||||||
|
this.name = name || "OidcAuthError";
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -46,10 +46,10 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
|
|||||||
await createTransport(smtpCfg)
|
await createTransport(smtpCfg)
|
||||||
.verify()
|
.verify()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
console.info("SMTP successfully connected");
|
console.info(`SMTP - Verified connection to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err: Error) => {
|
||||||
console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`);
|
console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT} - ${err.message}`);
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
GatewayTimeoutError,
|
GatewayTimeoutError,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
|
OidcAuthError,
|
||||||
RateLimitError,
|
RateLimitError,
|
||||||
ScimRequestError,
|
ScimRequestError,
|
||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
@ -83,7 +84,10 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
status: error.status,
|
status: error.status,
|
||||||
detail: error.detail
|
detail: error.detail
|
||||||
});
|
});
|
||||||
// Handle JWT errors and make them more human-readable for the end-user.
|
} else if (error instanceof OidcAuthError) {
|
||||||
|
void res
|
||||||
|
.status(HttpStatusCodes.InternalServerError)
|
||||||
|
.send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name });
|
||||||
} else if (error instanceof jwt.JsonWebTokenError) {
|
} else if (error instanceof jwt.JsonWebTokenError) {
|
||||||
const message = (() => {
|
const message = (() => {
|
||||||
if (error.message === JWTErrors.JwtExpired) {
|
if (error.message === JWTErrors.JwtExpired) {
|
||||||
|
@ -77,5 +77,21 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { sendMail };
|
const verify = async () => {
|
||||||
|
const isConnected = smtp
|
||||||
|
.verify()
|
||||||
|
.then(async () => {
|
||||||
|
logger.info("SMTP connected");
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
logger.error("SMTP error");
|
||||||
|
logger.error(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return isConnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { sendMail, verify };
|
||||||
};
|
};
|
||||||
|
@ -31,8 +31,7 @@ export const useCreateSecretV3 = ({
|
|||||||
secretKey,
|
secretKey,
|
||||||
secretValue,
|
secretValue,
|
||||||
secretComment,
|
secretComment,
|
||||||
skipMultilineEncoding,
|
skipMultilineEncoding
|
||||||
tagIds
|
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, {
|
const { data } = await apiRequest.post(`/api/v3/secrets/raw/${secretKey}`, {
|
||||||
secretPath,
|
secretPath,
|
||||||
@ -41,8 +40,7 @@ export const useCreateSecretV3 = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
secretValue,
|
secretValue,
|
||||||
secretComment,
|
secretComment,
|
||||||
skipMultilineEncoding,
|
skipMultilineEncoding
|
||||||
tagIds
|
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
@ -132,7 +132,6 @@ export type TCreateSecretsV3DTO = {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
type: SecretType;
|
type: SecretType;
|
||||||
tagIds?: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateSecretsV3DTO = {
|
export type TUpdateSecretsV3DTO = {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -11,12 +10,9 @@ import {
|
|||||||
faCircleInfo
|
faCircleInfo
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
|
||||||
import { useCreateIntegration } from "@app/hooks/api";
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
|
||||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||||
@ -87,61 +83,10 @@ const mappingBehaviors = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const schema = z
|
|
||||||
.object({
|
|
||||||
awsRegion: z.string().trim().min(1, { message: "AWS region is required" }),
|
|
||||||
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
|
|
||||||
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
|
|
||||||
secretPrefix: z.string().default(""),
|
|
||||||
secretName: z.string().trim().min(1).optional(),
|
|
||||||
mappingBehavior: z.nativeEnum(IntegrationMappingBehavior),
|
|
||||||
kmsKeyId: z.string().optional(),
|
|
||||||
shouldTag: z.boolean().optional(),
|
|
||||||
tags: z
|
|
||||||
.object({
|
|
||||||
key: z.string(),
|
|
||||||
value: z.string()
|
|
||||||
})
|
|
||||||
.array()
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(val) =>
|
|
||||||
val.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE ||
|
|
||||||
(val.mappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
|
|
||||||
val.secretName &&
|
|
||||||
val.secretName !== ""),
|
|
||||||
{
|
|
||||||
message: "Secret name must be defined for many-to-one integrations",
|
|
||||||
path: ["secretName"]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
type TFormSchema = z.infer<typeof schema>;
|
|
||||||
|
|
||||||
export default function AWSSecretManagerCreateIntegrationPage() {
|
export default function AWSSecretManagerCreateIntegrationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useCreateIntegration();
|
const { mutateAsync } = useCreateIntegration();
|
||||||
const {
|
|
||||||
control,
|
|
||||||
setValue,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<TFormSchema>({
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
defaultValues: {
|
|
||||||
shouldTag: false,
|
|
||||||
secretPath: "/",
|
|
||||||
secretPrefix: "",
|
|
||||||
mappingBehavior: IntegrationMappingBehavior.MANY_TO_ONE,
|
|
||||||
tags: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldTagState = watch("shouldTag");
|
|
||||||
const selectedSourceEnvironment = watch("sourceEnvironment");
|
|
||||||
const selectedAWSRegion = watch("awsRegion");
|
|
||||||
const selectedMappingBehavior = watch("mappingBehavior");
|
|
||||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||||
|
|
||||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||||
@ -149,6 +94,25 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
|||||||
(integrationAuthId as string) ?? ""
|
(integrationAuthId as string) ?? ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
|
const [selectedAWSRegion, setSelectedAWSRegion] = useState("");
|
||||||
|
const [selectedMappingBehavior, setSelectedMappingBehavior] = useState(
|
||||||
|
IntegrationMappingBehavior.MANY_TO_ONE
|
||||||
|
);
|
||||||
|
const [targetSecretName, setTargetSecretName] = useState("");
|
||||||
|
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
|
||||||
|
const [tagKey, setTagKey] = useState("");
|
||||||
|
const [tagValue, setTagValue] = useState("");
|
||||||
|
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||||
|
const [secretPrefix, setSecretPrefix] = useState("");
|
||||||
|
|
||||||
|
// const [path, setPath] = useState('');
|
||||||
|
// const [pathErrorText, setPathErrorText] = useState('');
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [shouldTag, setShouldTag] = useState(false);
|
||||||
|
|
||||||
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
|
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
|
||||||
useGetIntegrationAuthAwsKmsKeys({
|
useGetIntegrationAuthAwsKmsKeys({
|
||||||
integrationAuthId: String(integrationAuthId),
|
integrationAuthId: String(integrationAuthId),
|
||||||
@ -157,46 +121,63 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
setValue("sourceEnvironment", workspace.environments[0].slug);
|
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||||
setValue("awsRegion", awsRegions[0].slug);
|
setSelectedAWSRegion(awsRegions[0].slug);
|
||||||
}
|
}
|
||||||
}, [workspace]);
|
}, [workspace]);
|
||||||
|
|
||||||
const handleButtonClick = async ({
|
// const isValidAWSPath = (path: string) => {
|
||||||
secretName,
|
// const pattern = /^\/[\w./]+\/$/;
|
||||||
sourceEnvironment,
|
// return pattern.test(path) && path.length <= 2048;
|
||||||
awsRegion,
|
// }
|
||||||
secretPath,
|
|
||||||
shouldTag,
|
const handleButtonClick = async () => {
|
||||||
tags,
|
|
||||||
secretPrefix,
|
|
||||||
kmsKeyId,
|
|
||||||
mappingBehavior
|
|
||||||
}: TFormSchema) => {
|
|
||||||
try {
|
try {
|
||||||
|
if (!selectedMappingBehavior) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE &&
|
||||||
|
targetSecretName.trim() === ""
|
||||||
|
) {
|
||||||
|
setTargetSecretName("Secret name cannot be blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!integrationAuth?.id) return;
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
integrationAuthId: integrationAuth?.id,
|
integrationAuthId: integrationAuth?.id,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
app: secretName,
|
app: targetSecretName.trim(),
|
||||||
sourceEnvironment,
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
region: awsRegion,
|
region: selectedAWSRegion,
|
||||||
secretPath,
|
secretPath,
|
||||||
metadata: {
|
metadata: {
|
||||||
...(shouldTag
|
...(shouldTag
|
||||||
? {
|
? {
|
||||||
secretAWSTag: tags
|
secretAWSTag: [
|
||||||
|
{
|
||||||
|
key: tagKey,
|
||||||
|
value: tagValue
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(secretPrefix && { secretPrefix }),
|
...(secretPrefix && { secretPrefix }),
|
||||||
...(kmsKeyId && { kmsKeyId }),
|
...(kmsKeyId && { kmsKeyId }),
|
||||||
mappingBehavior
|
mappingBehavior: selectedMappingBehavior
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
setTargetSecretNameErrorText("");
|
||||||
|
|
||||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setIsLoading(false);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -210,305 +191,226 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
|||||||
<title>Set Up AWS Secrets Manager Integration</title>
|
<title>Set Up AWS Secrets Manager Integration</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<form onSubmit={handleSubmit(handleButtonClick)}>
|
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
<CardTitle
|
||||||
<CardTitle
|
className="px-6 text-left text-xl"
|
||||||
className="px-6 text-left text-xl"
|
subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Secrets Manager."
|
||||||
subTitle="Choose which environment in Infisical you want to sync to secerts in AWS Secrets Manager."
|
>
|
||||||
>
|
<div className="flex flex-row items-center">
|
||||||
<div className="flex flex-row items-center">
|
<div className="inline flex items-center">
|
||||||
<div className="flex items-center">
|
<Image
|
||||||
<Image
|
src="/images/integrations/Amazon Web Services.png"
|
||||||
src="/images/integrations/Amazon Web Services.png"
|
height={35}
|
||||||
height={35}
|
width={35}
|
||||||
width={35}
|
alt="AWS logo"
|
||||||
alt="AWS logo"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="ml-1.5">AWS Secrets Manager Integration </span>
|
|
||||||
<Link
|
|
||||||
href="https://infisical.com/docs/integrations/cloud/aws-secret-manager"
|
|
||||||
passHref
|
|
||||||
>
|
|
||||||
<a target="_blank" rel="noopener noreferrer">
|
|
||||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
|
||||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
|
||||||
Docs
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faArrowUpRightFromSquare}
|
|
||||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
<span className="ml-1.5">AWS Secrets Manager Integration </span>
|
||||||
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
<Link href="https://infisical.com/docs/integrations/cloud/aws-secret-manager" passHref>
|
||||||
<TabList>
|
<a target="_blank" rel="noopener noreferrer">
|
||||||
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
<Tab value={TabSections.Connection}>Connection</Tab>
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
<Tab value={TabSections.Options}>Options</Tab>
|
Docs
|
||||||
</div>
|
<FontAwesomeIcon
|
||||||
</TabList>
|
icon={faArrowUpRightFromSquare}
|
||||||
<TabPanel value={TabSections.Connection}>
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
<motion.div
|
|
||||||
key="panel-1"
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="sourceEnvironment"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Project Environment"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
dropdownContainerClassName="max-w-full"
|
|
||||||
value={field.value}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
field.onChange(val);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{workspace?.environments.map((sourceEnvironment) => (
|
|
||||||
<SelectItem
|
|
||||||
value={sourceEnvironment.slug}
|
|
||||||
key={`source-environment-${sourceEnvironment.slug}`}
|
|
||||||
>
|
|
||||||
{sourceEnvironment.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretPath"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Secrets Path"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<SecretPathInput {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="awsRegion"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="AWS region"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
dropdownContainerClassName="max-w-full"
|
|
||||||
>
|
|
||||||
{awsRegions.map((awsRegion) => (
|
|
||||||
<SelectItem
|
|
||||||
value={awsRegion.slug}
|
|
||||||
className="flex w-full justify-between"
|
|
||||||
key={`aws-environment-${awsRegion.slug}`}
|
|
||||||
>
|
|
||||||
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="mappingBehavior"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Mapping Behavior"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
dropdownContainerClassName="max-w-full"
|
|
||||||
>
|
|
||||||
{mappingBehaviors.map((option) => (
|
|
||||||
<SelectItem
|
|
||||||
value={option.value}
|
|
||||||
className="text-left"
|
|
||||||
key={`mapping-behavior-${option.value}`}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretName"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="AWS SM Secret Name"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder={`${workspace.name
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value={TabSections.Options}>
|
|
||||||
<motion.div
|
|
||||||
key="panel-1"
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
initial={{ opacity: 0, translateX: -30 }}
|
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
|
||||||
>
|
|
||||||
<div className="mt-2 ml-1">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="shouldTag"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Switch
|
|
||||||
id="tag-aws"
|
|
||||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
|
||||||
isChecked={value}
|
|
||||||
>
|
|
||||||
Tag in AWS Secrets Manager
|
|
||||||
</Switch>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{shouldTagState && (
|
</a>
|
||||||
<div className="mt-4 flex justify-between">
|
</Link>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="tags.0.key"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Tag Key"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Input placeholder="managed-by" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="tags.0.value"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Tag Value"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Input placeholder="infisical" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="secretPrefix"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Secret Prefix"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
<Input placeholder="INFISICAL_" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="kmsKeyId"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Encryption Key"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
dropdownContainerClassName="max-w-full"
|
|
||||||
>
|
|
||||||
{integrationAuthAwsKmsKeys?.length ? (
|
|
||||||
integrationAuthAwsKmsKeys.map((key) => {
|
|
||||||
return (
|
|
||||||
<SelectItem
|
|
||||||
value={key.id as string}
|
|
||||||
key={`repo-id-${key.id}`}
|
|
||||||
className="w-[28.4rem] text-sm"
|
|
||||||
>
|
|
||||||
{key.alias}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<SelectItem isDisabled value="no-keys" key="no-keys">
|
|
||||||
No KMS keys available
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
<Button
|
|
||||||
color="mineshaft"
|
|
||||||
variant="outline_bg"
|
|
||||||
type="submit"
|
|
||||||
className="mb-6 mt-2 ml-auto mr-6"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Create Integration
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
|
||||||
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
|
||||||
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="mt-4 text-sm text-mineshaft-300">
|
</CardTitle>
|
||||||
After creating an integration, your secrets will start syncing immediately. This might
|
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
||||||
cause an unexpected override of current secrets in AWS Secrets Manager with secrets from
|
<TabList>
|
||||||
Infisical.
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
</span>
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={TabSections.Connection}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<FormControl label="Project Environment">
|
||||||
|
<Select
|
||||||
|
value={selectedSourceEnvironment}
|
||||||
|
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`flyio-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Secrets Path">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="AWS Region">
|
||||||
|
<Select
|
||||||
|
value={selectedAWSRegion}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setSelectedAWSRegion(val);
|
||||||
|
setKmsKeyId("");
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{awsRegions.map((awsRegion) => (
|
||||||
|
<SelectItem
|
||||||
|
value={awsRegion.slug}
|
||||||
|
className="flex w-full justify-between"
|
||||||
|
key={`aws-environment-${awsRegion.slug}`}
|
||||||
|
>
|
||||||
|
{awsRegion.name} <Badge variant="success">{awsRegion.slug}</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Mapping Behavior">
|
||||||
|
<Select
|
||||||
|
value={selectedMappingBehavior}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setSelectedMappingBehavior(val as IntegrationMappingBehavior);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500 text-left"
|
||||||
|
>
|
||||||
|
{mappingBehaviors.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
value={option.value}
|
||||||
|
className="text-left"
|
||||||
|
key={`aws-environment-${option.value}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{selectedMappingBehavior === IntegrationMappingBehavior.MANY_TO_ONE && (
|
||||||
|
<FormControl
|
||||||
|
label="AWS SM Secret Name"
|
||||||
|
errorText={targetSecretNameErrorText}
|
||||||
|
isError={targetSecretNameErrorText !== "" ?? false}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={`${workspace.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
|
||||||
|
value={targetSecretName}
|
||||||
|
onChange={(e) => setTargetSecretName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Options}>
|
||||||
|
<motion.div
|
||||||
|
key="panel-1"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: -30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<div className="mt-2 ml-1">
|
||||||
|
<Switch
|
||||||
|
id="tag-aws"
|
||||||
|
onCheckedChange={() => setShouldTag(!shouldTag)}
|
||||||
|
isChecked={shouldTag}
|
||||||
|
>
|
||||||
|
Tag in AWS Secrets Manager
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
{shouldTag && (
|
||||||
|
<div className="mt-4 flex justify-between">
|
||||||
|
<FormControl label="Tag Key">
|
||||||
|
<Input
|
||||||
|
placeholder="managed-by"
|
||||||
|
value={tagKey}
|
||||||
|
onChange={(e) => setTagKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Tag Value">
|
||||||
|
<Input
|
||||||
|
placeholder="infisical"
|
||||||
|
value={tagValue}
|
||||||
|
onChange={(e) => setTagValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormControl label="Secret Prefix" className="mt-4">
|
||||||
|
<Input
|
||||||
|
value={secretPrefix}
|
||||||
|
onChange={(e) => setSecretPrefix(e.target.value)}
|
||||||
|
placeholder="INFISICAL_"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Encryption Key" className="mt-4">
|
||||||
|
<Select
|
||||||
|
value={kmsKeyId}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
if (e === "no-keys") return;
|
||||||
|
setKmsKeyId(e);
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
{integrationAuthAwsKmsKeys?.length ? (
|
||||||
|
integrationAuthAwsKmsKeys.map((key) => {
|
||||||
|
return (
|
||||||
|
<SelectItem
|
||||||
|
value={key.id as string}
|
||||||
|
key={`repo-id-${key.id}`}
|
||||||
|
className="w-[28.4rem] text-sm"
|
||||||
|
>
|
||||||
|
{key.alias}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<SelectItem isDisabled value="no-keys" key="no-keys">
|
||||||
|
No KMS keys available
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
color="mineshaft"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="mb-6 mt-2 ml-auto mr-6"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create Integration
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||||
|
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||||
|
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<span className="mt-4 text-sm text-mineshaft-300">
|
||||||
|
After creating an integration, your secrets will start syncing immediately. This might
|
||||||
|
cause an unexpected override of current secrets in AWS Secrets Manager with secrets from
|
||||||
|
Infisical.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
@ -9,14 +9,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import {
|
import { Checkbox, ContentLoader, Pagination, Tooltip } from "@app/components/v2";
|
||||||
Checkbox,
|
|
||||||
ContentLoader,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Pagination,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
@ -48,10 +41,7 @@ import { SecretDropzone } from "./components/SecretDropzone";
|
|||||||
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
|
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
|
||||||
import { SnapshotView } from "./components/SnapshotView";
|
import { SnapshotView } from "./components/SnapshotView";
|
||||||
import {
|
import {
|
||||||
PopUpNames,
|
|
||||||
StoreProvider,
|
StoreProvider,
|
||||||
usePopUpAction,
|
|
||||||
usePopUpState,
|
|
||||||
useSelectedSecretActions,
|
useSelectedSecretActions,
|
||||||
useSelectedSecrets
|
useSelectedSecrets
|
||||||
} from "./SecretMainPage.store";
|
} from "./SecretMainPage.store";
|
||||||
@ -133,9 +123,6 @@ const SecretMainPageContent = () => {
|
|||||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
||||||
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
||||||
|
|
||||||
const createSecretPopUp = usePopUpState(PopUpNames.CreateSecretForm);
|
|
||||||
const { togglePopUp } = usePopUpAction();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isWorkspaceLoading &&
|
!isWorkspaceLoading &&
|
||||||
@ -533,24 +520,13 @@ const SecretMainPageContent = () => {
|
|||||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Modal
|
<CreateSecretForm
|
||||||
isOpen={createSecretPopUp.isOpen}
|
environment={environment}
|
||||||
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
|
workspaceId={workspaceId}
|
||||||
>
|
secretPath={secretPath}
|
||||||
<ModalContent
|
autoCapitalize={currentWorkspace?.autoCapitalization}
|
||||||
title="Create Secret"
|
isProtectedBranch={isProtectedBranch}
|
||||||
subTitle="Add a secret to this particular environment and folder"
|
/>
|
||||||
bodyClassName="overflow-visible"
|
|
||||||
>
|
|
||||||
<CreateSecretForm
|
|
||||||
environment={environment}
|
|
||||||
workspaceId={workspaceId}
|
|
||||||
secretPath={secretPath}
|
|
||||||
autoCapitalize={currentWorkspace?.autoCapitalization}
|
|
||||||
isProtectedBranch={isProtectedBranch}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<SecretDropzone
|
<SecretDropzone
|
||||||
secrets={secrets}
|
secrets={secrets}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
import { ClipboardEvent } from "react";
|
import { ClipboardEvent } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, MultiSelect } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
|
||||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||||
import { useCreateSecretV3, useGetWsTags } from "@app/hooks/api";
|
import { useCreateSecretV3 } from "@app/hooks/api";
|
||||||
import { SecretType } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
import { PopUpNames, usePopUpAction, usePopUpState } from "../../SecretMainPage.store";
|
||||||
|
|
||||||
const typeSchema = z.object({
|
const typeSchema = z.object({
|
||||||
key: z.string().trim().min(1, { message: "Secret key is required" }),
|
key: z.string().trim().min(1, { message: "Secret key is required" }),
|
||||||
value: z.string().optional(),
|
value: z.string().optional()
|
||||||
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type TFormSchema = z.infer<typeof typeSchema>;
|
type TFormSchema = z.infer<typeof typeSchema>;
|
||||||
@ -47,16 +43,12 @@ export const CreateSecretForm = ({
|
|||||||
setValue,
|
setValue,
|
||||||
formState: { errors, isSubmitting }
|
formState: { errors, isSubmitting }
|
||||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||||
const { closePopUp } = usePopUpAction();
|
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
|
||||||
|
const { closePopUp, togglePopUp } = usePopUpAction();
|
||||||
|
|
||||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
const { permission } = useProjectPermission();
|
|
||||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
|
||||||
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
|
||||||
canReadTags ? workspaceId : ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
|
const handleFormSubmit = async ({ key, value }: TFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await createSecretV3({
|
await createSecretV3({
|
||||||
environment,
|
environment,
|
||||||
@ -65,8 +57,7 @@ export const CreateSecretForm = ({
|
|||||||
secretKey: key,
|
secretKey: key,
|
||||||
secretValue: value || "",
|
secretValue: value || "",
|
||||||
secretComment: "",
|
secretComment: "",
|
||||||
type: SecretType.Shared,
|
type: SecretType.Shared
|
||||||
tagIds: tags?.map((el) => el.value)
|
|
||||||
});
|
});
|
||||||
closePopUp(PopUpNames.CreateSecretForm);
|
closePopUp(PopUpNames.CreateSecretForm);
|
||||||
reset();
|
reset();
|
||||||
@ -97,90 +88,67 @@ export const CreateSecretForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
<Modal
|
||||||
<FormControl
|
isOpen={isOpen}
|
||||||
label="Key"
|
onOpenChange={(state) => togglePopUp(PopUpNames.CreateSecretForm, state)}
|
||||||
isRequired
|
>
|
||||||
isError={Boolean(errors?.key)}
|
<ModalContent
|
||||||
errorText={errors?.key?.message}
|
title="Create secret"
|
||||||
|
subTitle="Add a secret to the particular environment and folder"
|
||||||
>
|
>
|
||||||
<Input
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
{...register("key")}
|
|
||||||
placeholder="Type your secret name"
|
|
||||||
onPaste={handlePaste}
|
|
||||||
autoCapitalization={autoCapitalize}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="value"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Value"
|
label="Key"
|
||||||
isError={Boolean(errors?.value)}
|
isRequired
|
||||||
errorText={errors?.value?.message}
|
isError={Boolean(errors?.key)}
|
||||||
|
errorText={errors?.key?.message}
|
||||||
>
|
>
|
||||||
<InfisicalSecretInput
|
<Input
|
||||||
{...field}
|
{...register("key")}
|
||||||
environment={environment}
|
placeholder="Type your secret name"
|
||||||
secretPath={secretPath}
|
onPaste={handlePaste}
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
autoCapitalization={autoCapitalize}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
<Controller
|
||||||
/>
|
control={control}
|
||||||
<Controller
|
name="value"
|
||||||
control={control}
|
render={({ field }) => (
|
||||||
name="tags"
|
<FormControl
|
||||||
render={({ field }) => (
|
label="Value"
|
||||||
<FormControl
|
isError={Boolean(errors?.value)}
|
||||||
label="Tags"
|
errorText={errors?.value?.message}
|
||||||
isError={Boolean(errors?.value)}
|
>
|
||||||
errorText={errors?.value?.message}
|
<InfisicalSecretInput
|
||||||
helperText={
|
{...field}
|
||||||
!canReadTags ? (
|
environment={environment}
|
||||||
<div className="flex items-center space-x-2">
|
secretPath={secretPath}
|
||||||
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||||
<span>You do not have permission to read tags.</span>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
) : (
|
)}
|
||||||
""
|
/>
|
||||||
)
|
<div className="mt-7 flex items-center">
|
||||||
}
|
<Button
|
||||||
>
|
isDisabled={isSubmitting}
|
||||||
<MultiSelect
|
isLoading={isSubmitting}
|
||||||
className="w-full"
|
key="layout-create-project-submit"
|
||||||
placeholder="Select tags to assign to secret..."
|
className="mr-4"
|
||||||
isMulti
|
type="submit"
|
||||||
name="tagIds"
|
>
|
||||||
isDisabled={!canReadTags}
|
Create Secret
|
||||||
isLoading={isTagsLoading}
|
</Button>
|
||||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
<Button
|
||||||
value={field.value}
|
key="layout-cancel-create-project"
|
||||||
onChange={field.onChange}
|
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
|
||||||
/>
|
variant="plain"
|
||||||
</FormControl>
|
colorSchema="secondary"
|
||||||
)}
|
>
|
||||||
/>
|
Cancel
|
||||||
<div className="mt-7 flex items-center">
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
isDisabled={isSubmitting}
|
</form>
|
||||||
isLoading={isSubmitting}
|
</ModalContent>
|
||||||
key="layout-create-project-submit"
|
</Modal>
|
||||||
className="mr-4"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Create Secret
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
key="layout-cancel-create-project"
|
|
||||||
onClick={() => closePopUp(PopUpNames.CreateSecretForm)}
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1116,23 +1116,13 @@ export const SecretOverviewPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<CreateSecretForm
|
||||||
|
secretPath={secretPath}
|
||||||
isOpen={popUp.addSecretsInAllEnvs.isOpen}
|
isOpen={popUp.addSecretsInAllEnvs.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
getSecretByKey={getSecretByKey}
|
||||||
>
|
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
|
||||||
<ModalContent
|
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
||||||
className="max-h-[80vh]"
|
/>
|
||||||
bodyClassName="overflow-visible"
|
|
||||||
title="Create Secrets"
|
|
||||||
subTitle="Create a secret across multiple environments"
|
|
||||||
>
|
|
||||||
<CreateSecretForm
|
|
||||||
secretPath={secretPath}
|
|
||||||
getSecretByKey={getSecretByKey}
|
|
||||||
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp.addFolder.isOpen}
|
isOpen={popUp.addFolder.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ClipboardEvent } from "react";
|
import { ClipboardEvent } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faTriangleExclamation, faWarning } from "@fortawesome/free-solid-svg-icons";
|
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -13,7 +13,8 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
MultiSelect,
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
@ -24,20 +25,14 @@ import {
|
|||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||||
import {
|
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||||
useCreateFolder,
|
|
||||||
useCreateSecretV3,
|
|
||||||
useGetWsTags,
|
|
||||||
useUpdateSecretV3
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||||
|
|
||||||
const typeSchema = z
|
const typeSchema = z
|
||||||
.object({
|
.object({
|
||||||
key: z.string().trim().min(1, "Key is required"),
|
key: z.string().trim().min(1, "Key is required"),
|
||||||
value: z.string().optional(),
|
value: z.string().optional(),
|
||||||
environments: z.record(z.boolean().optional()),
|
environments: z.record(z.boolean().optional())
|
||||||
tags: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).optional()
|
|
||||||
})
|
})
|
||||||
.refine((data) => data.key !== undefined, {
|
.refine((data) => data.key !== undefined, {
|
||||||
message: "Please enter secret name"
|
message: "Please enter secret name"
|
||||||
@ -49,10 +44,18 @@ type Props = {
|
|||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
|
getSecretByKey: (slug: string, key: string) => SecretV3RawSanitized | undefined;
|
||||||
// modal props
|
// modal props
|
||||||
|
isOpen?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onTogglePopUp: (isOpen: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }: Props) => {
|
export const CreateSecretForm = ({
|
||||||
|
secretPath = "/",
|
||||||
|
isOpen,
|
||||||
|
getSecretByKey,
|
||||||
|
onClose,
|
||||||
|
onTogglePopUp
|
||||||
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -66,18 +69,14 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
|||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const environments = currentWorkspace?.environments || [];
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
|
||||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||||
const { mutateAsync: createFolder } = useCreateFolder();
|
const { mutateAsync: createFolder } = useCreateFolder();
|
||||||
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
|
|
||||||
canReadTags ? workspaceId : ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFormSubmit = async ({ key, value, environments: selectedEnv, tags }: TFormSchema) => {
|
const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => {
|
||||||
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
|
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
|
||||||
const isEnvironmentsSelected = environmentsSelected.length;
|
const isEnvironmentsSelected = environmentsSelected.length;
|
||||||
|
|
||||||
@ -121,8 +120,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
|||||||
secretPath,
|
secretPath,
|
||||||
secretKey: key,
|
secretKey: key,
|
||||||
secretValue: value || "",
|
secretValue: value || "",
|
||||||
type: SecretType.Shared,
|
type: SecretType.Shared
|
||||||
tagIds: tags?.map((el) => el.value)
|
|
||||||
})),
|
})),
|
||||||
environment
|
environment
|
||||||
};
|
};
|
||||||
@ -136,8 +134,7 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
|||||||
secretKey: key,
|
secretKey: key,
|
||||||
secretValue: value || "",
|
secretValue: value || "",
|
||||||
secretComment: "",
|
secretComment: "",
|
||||||
type: SecretType.Shared,
|
type: SecretType.Shared
|
||||||
tagIds: tags?.map((el) => el.value)
|
|
||||||
})),
|
})),
|
||||||
environment
|
environment
|
||||||
};
|
};
|
||||||
@ -200,136 +197,114 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||||
<FormControl
|
<ModalContent
|
||||||
label="Key"
|
className="max-h-[80vh] overflow-y-auto"
|
||||||
isRequired
|
title="Bulk Create & Update"
|
||||||
isError={Boolean(errors?.key)}
|
subTitle="Create & update a secret across many environments"
|
||||||
errorText={errors?.key?.message}
|
|
||||||
>
|
>
|
||||||
<Input
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
{...register("key")}
|
|
||||||
placeholder="Type your secret name"
|
|
||||||
onPaste={handlePaste}
|
|
||||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="value"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Value"
|
label="Key"
|
||||||
isError={Boolean(errors?.value)}
|
isRequired
|
||||||
errorText={errors?.value?.message}
|
isError={Boolean(errors?.key)}
|
||||||
|
errorText={errors?.key?.message}
|
||||||
>
|
>
|
||||||
<InfisicalSecretInput
|
<Input
|
||||||
{...field}
|
{...register("key")}
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
placeholder="Type your secret name"
|
||||||
|
onPaste={handlePaste}
|
||||||
|
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
<Controller
|
||||||
/>
|
control={control}
|
||||||
<Controller
|
name="value"
|
||||||
control={control}
|
render={({ field }) => (
|
||||||
name="tags"
|
<FormControl
|
||||||
render={({ field }) => (
|
label="Value"
|
||||||
<FormControl
|
isError={Boolean(errors?.value)}
|
||||||
label="Tags"
|
errorText={errors?.value?.message}
|
||||||
isError={Boolean(errors?.value)}
|
>
|
||||||
errorText={errors?.value?.message}
|
<InfisicalSecretInput
|
||||||
helperText={
|
{...field}
|
||||||
!canReadTags ? (
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||||
<div className="flex items-center space-x-2">
|
/>
|
||||||
<FontAwesomeIcon icon={faTriangleExclamation} className="text-yellow-400" />
|
</FormControl>
|
||||||
<span>You do not have permission to read tags.</span>
|
)}
|
||||||
</div>
|
/>
|
||||||
) : (
|
<FormLabel label="Environments" className="mb-2" />
|
||||||
""
|
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
||||||
|
{environments
|
||||||
|
.filter((environmentSlug) =>
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: environmentSlug.slug,
|
||||||
|
secretPath,
|
||||||
|
secretName: "*",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
.map((env) => {
|
||||||
>
|
return (
|
||||||
<MultiSelect
|
<Controller
|
||||||
className="w-full"
|
name={`environments.${env.slug}`}
|
||||||
placeholder="Select tags to assign to secrets..."
|
key={`secret-input-${env.slug}`}
|
||||||
isMulti
|
control={control}
|
||||||
name="tagIds"
|
render={({ field }) => (
|
||||||
isDisabled={!canReadTags}
|
<Checkbox
|
||||||
isLoading={isTagsLoading}
|
isChecked={field.value}
|
||||||
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
|
onCheckedChange={field.onChange}
|
||||||
value={field.value}
|
id={`secret-input-${env.slug}`}
|
||||||
onChange={field.onChange}
|
className="!justify-start"
|
||||||
/>
|
>
|
||||||
</FormControl>
|
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
||||||
)}
|
<span title={env.name} className="truncate">
|
||||||
/>
|
{env.name}
|
||||||
<FormLabel label="Environments" className="mb-2" />
|
</span>
|
||||||
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
|
<span>
|
||||||
{environments
|
{getSecretByKey(env.slug, newSecretKey) && (
|
||||||
.filter((environmentSlug) =>
|
<Tooltip
|
||||||
permission.can(
|
className="max-w-[150px]"
|
||||||
ProjectPermissionActions.Create,
|
content="Secret already exists, and it will be overwritten"
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
>
|
||||||
environment: environmentSlug.slug,
|
<FontAwesomeIcon
|
||||||
secretPath,
|
icon={faWarning}
|
||||||
secretName: "*",
|
className="ml-1 text-yellow-400"
|
||||||
secretTags: ["*"]
|
/>
|
||||||
})
|
</Tooltip>
|
||||||
)
|
)}
|
||||||
)
|
</span>
|
||||||
.map((env) => {
|
</span>
|
||||||
return (
|
</Checkbox>
|
||||||
<Controller
|
)}
|
||||||
name={`environments.${env.slug}`}
|
/>
|
||||||
key={`secret-input-${env.slug}`}
|
);
|
||||||
control={control}
|
})}
|
||||||
render={({ field }) => (
|
</div>
|
||||||
<Checkbox
|
<div className="mt-7 flex items-center">
|
||||||
isChecked={field.value}
|
<Button
|
||||||
onCheckedChange={field.onChange}
|
isDisabled={isSubmitting}
|
||||||
id={`secret-input-${env.slug}`}
|
isLoading={isSubmitting}
|
||||||
className="!justify-start"
|
key="layout-create-project-submit"
|
||||||
>
|
className="mr-4"
|
||||||
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
|
type="submit"
|
||||||
<span title={env.name} className="truncate">
|
>
|
||||||
{env.name}
|
Create Secret
|
||||||
</span>
|
</Button>
|
||||||
<span>
|
<Button
|
||||||
{getSecretByKey(env.slug, newSecretKey) && (
|
key="layout-cancel-create-project"
|
||||||
<Tooltip
|
onClick={onClose}
|
||||||
className="max-w-[150px]"
|
variant="plain"
|
||||||
content="Secret already exists, and it will be overwritten"
|
colorSchema="secondary"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
|
Cancel
|
||||||
</Tooltip>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</span>
|
</form>
|
||||||
</span>
|
</ModalContent>
|
||||||
</Checkbox>
|
</Modal>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="mt-7 flex items-center">
|
|
||||||
<Button
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
key="layout-create-project-submit"
|
|
||||||
className="mr-4"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Create Secret
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
key="layout-cancel-create-project"
|
|
||||||
onClick={onClose}
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user