mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-20 01:48:03 +00:00
Compare commits
19 Commits
misc/updat
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
18c0d2fd6f | |||
c1fb8f47bf | |||
990eddeb32 | |||
ce01f8d099 | |||
faf6708b00 | |||
a58d6ebdac | |||
818b136836 | |||
0cdade6a2d | |||
6561a9c7be | |||
86aaa486b4 | |||
9880977098 | |||
b93aaffe77 | |||
1ea0d55dd1 | |||
3fff272cb3 | |||
2559809eac | |||
f32abbdc25 | |||
a6f750fafb | |||
522dd0836e | |||
e461787c78 |
@ -252,6 +252,7 @@ export const FOLDERS = {
|
|||||||
name: "The new name of the folder.",
|
name: "The new name of the folder.",
|
||||||
path: "The path of the folder to update.",
|
path: "The path of the folder to update.",
|
||||||
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
||||||
|
projectSlug: "The slug of the project where the folder is located.",
|
||||||
workspaceId: "The ID of the project where the folder is located."
|
workspaceId: "The ID of the project where the folder is located."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
|
@ -5,8 +5,13 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
export const maintenanceMode = fp(async (fastify) => {
|
export const maintenanceMode = fp(async (fastify) => {
|
||||||
fastify.addHook("onRequest", async (req) => {
|
fastify.addHook("onRequest", async (req) => {
|
||||||
const serverEnvs = getConfig();
|
const serverEnvs = getConfig();
|
||||||
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET" && serverEnvs.MAINTENANCE_MODE) {
|
if (serverEnvs.MAINTENANCE_MODE) {
|
||||||
throw new Error("Infisical is in maintenance mode. Please try again later.");
|
// 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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -538,8 +538,10 @@ export const registerRoutes = async (
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
folderVersionDAL,
|
folderVersionDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
snapshotService
|
snapshotService,
|
||||||
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
integrationAuthDAL,
|
integrationAuthDAL,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
|
@ -143,8 +143,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
|
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
app: z.string().trim().describe(INTEGRATION.UPDATE.app),
|
app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app),
|
||||||
appId: z.string().trim().describe(INTEGRATION.UPDATE.appId),
|
appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId),
|
||||||
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
|
isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive),
|
||||||
secretPath: z
|
secretPath: z
|
||||||
.string()
|
.string()
|
||||||
@ -154,7 +154,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(INTEGRATION.UPDATE.secretPath),
|
.describe(INTEGRATION.UPDATE.secretPath),
|
||||||
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
||||||
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -127,6 +127,70 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/batch",
|
||||||
|
method: "PATCH",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Update folders by batch",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
body: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(FOLDERS.UPDATE.projectSlug),
|
||||||
|
folders: z
|
||||||
|
.object({
|
||||||
|
id: z.string().describe(FOLDERS.UPDATE.folderId),
|
||||||
|
environment: z.string().trim().describe(FOLDERS.UPDATE.environment),
|
||||||
|
name: z.string().trim().describe(FOLDERS.UPDATE.name),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(FOLDERS.UPDATE.path)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
folders: SecretFoldersSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { newFolders, oldFolders, projectId } = await server.services.folder.updateManyFolders({
|
||||||
|
...req.body,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
req.body.folders.map(async (folder, index) => {
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_FOLDER,
|
||||||
|
metadata: {
|
||||||
|
environment: oldFolders[index].envId,
|
||||||
|
folderId: oldFolders[index].id,
|
||||||
|
folderPath: folder.path,
|
||||||
|
newFolderName: newFolders[index].name,
|
||||||
|
oldFolderName: oldFolders[index].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return { folders: newFolders };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TODO(daniel): Expose this route in api reference and write docs for it.
|
// TODO(daniel): Expose this route in api reference and write docs for it.
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
@ -9,9 +9,12 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CreateSecretCommand,
|
CreateSecretCommand,
|
||||||
|
DescribeSecretCommand,
|
||||||
GetSecretValueCommand,
|
GetSecretValueCommand,
|
||||||
ResourceNotFoundException,
|
ResourceNotFoundException,
|
||||||
SecretsManagerClient,
|
SecretsManagerClient,
|
||||||
|
TagResourceCommand,
|
||||||
|
UntagResourceCommand,
|
||||||
UpdateSecretCommand
|
UpdateSecretCommand
|
||||||
} from "@aws-sdk/client-secrets-manager";
|
} from "@aws-sdk/client-secrets-manager";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
@ -574,6 +577,7 @@ const syncSecretsAWSSecretManager = async ({
|
|||||||
if (awsSecretManagerSecret?.SecretString) {
|
if (awsSecretManagerSecret?.SecretString) {
|
||||||
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
|
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) {
|
if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) {
|
||||||
await secretsManager.send(
|
await secretsManager.send(
|
||||||
new UpdateSecretCommand({
|
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) {
|
} catch (err) {
|
||||||
|
// case when AWS manager can't find the specified secret
|
||||||
if (err instanceof ResourceNotFoundException && secretsManager) {
|
if (err instanceof ResourceNotFoundException && secretsManager) {
|
||||||
await secretsManager.send(
|
await secretsManager.send(
|
||||||
new CreateSecretCommand({
|
new CreateSecretCommand({
|
||||||
|
@ -103,7 +103,8 @@ export const integrationServiceFactory = ({
|
|||||||
owner,
|
owner,
|
||||||
isActive,
|
isActive,
|
||||||
environment,
|
environment,
|
||||||
secretPath
|
secretPath,
|
||||||
|
metadata
|
||||||
}: TUpdateIntegrationDTO) => {
|
}: TUpdateIntegrationDTO) => {
|
||||||
const integration = await integrationDAL.findById(id);
|
const integration = await integrationDAL.findById(id);
|
||||||
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||||
@ -127,7 +128,17 @@ export const integrationServiceFactory = ({
|
|||||||
appId,
|
appId,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
owner,
|
owner,
|
||||||
secretPath
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
...(integration.metadata as object),
|
||||||
|
...metadata
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await secretQueueService.syncIntegrations({
|
||||||
|
environment: folder.environment.slug,
|
||||||
|
secretPath,
|
||||||
|
projectId: folder.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedIntegration;
|
return updatedIntegration;
|
||||||
|
@ -33,13 +33,27 @@ export type TCreateIntegrationDTO = {
|
|||||||
|
|
||||||
export type TUpdateIntegrationDTO = {
|
export type TUpdateIntegrationDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
app: string;
|
app?: string;
|
||||||
appId: string;
|
appId?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
targetEnvironment: string;
|
targetEnvironment: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
environment: 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">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteIntegrationDTO = {
|
export type TDeleteIntegrationDTO = {
|
||||||
|
@ -8,9 +8,16 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
|
|||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||||
import { TCreateFolderDTO, TDeleteFolderDTO, TGetFolderDTO, TUpdateFolderDTO } from "./secret-folder-types";
|
import {
|
||||||
|
TCreateFolderDTO,
|
||||||
|
TDeleteFolderDTO,
|
||||||
|
TGetFolderDTO,
|
||||||
|
TUpdateFolderDTO,
|
||||||
|
TUpdateManyFoldersDTO
|
||||||
|
} from "./secret-folder-types";
|
||||||
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
|
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
|
||||||
|
|
||||||
type TSecretFolderServiceFactoryDep = {
|
type TSecretFolderServiceFactoryDep = {
|
||||||
@ -19,6 +26,7 @@ type TSecretFolderServiceFactoryDep = {
|
|||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
|
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
|
||||||
@ -28,7 +36,8 @@ export const secretFolderServiceFactory = ({
|
|||||||
snapshotService,
|
snapshotService,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
folderVersionDAL
|
folderVersionDAL,
|
||||||
|
projectDAL
|
||||||
}: TSecretFolderServiceFactoryDep) => {
|
}: TSecretFolderServiceFactoryDep) => {
|
||||||
const createFolder = async ({
|
const createFolder = async ({
|
||||||
projectId,
|
projectId,
|
||||||
@ -116,6 +125,105 @@ export const secretFolderServiceFactory = ({
|
|||||||
return folder;
|
return folder;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateManyFolders = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectSlug,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
folders
|
||||||
|
}: TUpdateManyFoldersDTO) => {
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
if (!project) {
|
||||||
|
throw new BadRequestError({ message: "Project not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
folders.forEach(({ environment, path: secretPath }) => {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await folderDAL.transaction(async (tx) =>
|
||||||
|
Promise.all(
|
||||||
|
folders.map(async (newFolder) => {
|
||||||
|
const { environment, path: secretPath, id, name } = newFolder;
|
||||||
|
|
||||||
|
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
|
||||||
|
if (!parentFolder) {
|
||||||
|
throw new BadRequestError({ message: "Secret path not found", name: "Batch update folder" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.findOne({ projectId: project.id, slug: environment });
|
||||||
|
if (!env) {
|
||||||
|
throw new BadRequestError({ message: "Environment not found", name: "Batch update folder" });
|
||||||
|
}
|
||||||
|
const folder = await folderDAL
|
||||||
|
.findOne({ envId: env.id, id, parentId: parentFolder.id })
|
||||||
|
// now folder api accepts id based change
|
||||||
|
// this is for cli backward compatiability and when cli removes this, we will remove this logic
|
||||||
|
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
}
|
||||||
|
if (name !== folder.name) {
|
||||||
|
// ensure that new folder name is unique
|
||||||
|
const folderToCheck = await folderDAL.findOne({
|
||||||
|
name,
|
||||||
|
envId: env.id,
|
||||||
|
parentId: parentFolder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folderToCheck) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Folder with specified name already exists",
|
||||||
|
name: "Batch update folder"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [doc] = await folderDAL.update(
|
||||||
|
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
|
||||||
|
{ name },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await folderVersionDAL.create(
|
||||||
|
{
|
||||||
|
name: doc.name,
|
||||||
|
envId: doc.envId,
|
||||||
|
version: doc.version,
|
||||||
|
folderId: doc.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (!doc) {
|
||||||
|
throw new BadRequestError({ message: "Folder not found", name: "Batch update folder" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { oldFolder: folder, newFolder: doc };
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(result.map(async (res) => snapshotService.performSnapshot(res.newFolder.parentId as string)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId: project.id,
|
||||||
|
newFolders: result.map((res) => res.newFolder),
|
||||||
|
oldFolders: result.map((res) => res.oldFolder)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const updateFolder = async ({
|
const updateFolder = async ({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
@ -151,6 +259,21 @@ export const secretFolderServiceFactory = ({
|
|||||||
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
|
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
|
||||||
|
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
if (name !== folder.name) {
|
||||||
|
// ensure that new folder name is unique
|
||||||
|
const folderToCheck = await folderDAL.findOne({
|
||||||
|
name,
|
||||||
|
envId: env.id,
|
||||||
|
parentId: parentFolder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folderToCheck) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Folder with specified name already exists",
|
||||||
|
name: "Update folder"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newFolder = await folderDAL.transaction(async (tx) => {
|
const newFolder = await folderDAL.transaction(async (tx) => {
|
||||||
const [doc] = await folderDAL.update({ envId: env.id, id: folder.id, parentId: parentFolder.id }, { name }, tx);
|
const [doc] = await folderDAL.update({ envId: env.id, id: folder.id, parentId: parentFolder.id }, { name }, tx);
|
||||||
@ -239,6 +362,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
createFolder,
|
createFolder,
|
||||||
updateFolder,
|
updateFolder,
|
||||||
|
updateManyFolders,
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
getFolders
|
getFolders
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,16 @@ export type TUpdateFolderDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateManyFoldersDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
folders: {
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteFolderDTO = {
|
export type TDeleteFolderDTO = {
|
||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -29,7 +29,9 @@ Prerequisites:
|
|||||||
"secretsmanager:GetSecretValue",
|
"secretsmanager:GetSecretValue",
|
||||||
"secretsmanager:CreateSecret",
|
"secretsmanager:CreateSecret",
|
||||||
"secretsmanager:UpdateSecret",
|
"secretsmanager:UpdateSecret",
|
||||||
|
"secretsmanager:DescribeSecret", // if you need to add tags to secrets
|
||||||
"secretsmanager:TagResource", // 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:ListKeys", // if you need to specify the KMS key
|
||||||
"kms:ListAliases", // 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
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
TGetFoldersByEnvDTO,
|
TGetFoldersByEnvDTO,
|
||||||
TGetProjectFoldersDTO,
|
TGetProjectFoldersDTO,
|
||||||
TSecretFolder,
|
TSecretFolder,
|
||||||
|
TUpdateFolderBatchDTO,
|
||||||
TUpdateFolderDTO
|
TUpdateFolderDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@ -190,3 +191,43 @@ export const useDeleteFolder = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateFolderBatch = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TUpdateFolderBatchDTO>({
|
||||||
|
mutationFn: async ({ projectSlug, folders }) => {
|
||||||
|
const { data } = await apiRequest.patch("/api/v1/folders/batch", {
|
||||||
|
projectSlug,
|
||||||
|
folders
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId, folders }) => {
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
folderQueryKeys.getSecretFolders({
|
||||||
|
projectId,
|
||||||
|
environment: folder.environment,
|
||||||
|
path: folder.path
|
||||||
|
})
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.list({
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment: folder.environment,
|
||||||
|
directory: folder.path
|
||||||
|
})
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.count({
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment: folder.environment,
|
||||||
|
directory: folder.path
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -36,3 +36,14 @@ export type TDeleteFolderDTO = {
|
|||||||
folderId: string;
|
folderId: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUpdateFolderBatchDTO = {
|
||||||
|
projectId: string;
|
||||||
|
projectSlug: string;
|
||||||
|
folders: {
|
||||||
|
name: string;
|
||||||
|
environment: string;
|
||||||
|
id: string;
|
||||||
|
path?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
@ -21,9 +21,7 @@ import {
|
|||||||
faNetworkWired,
|
faNetworkWired,
|
||||||
faPlug,
|
faPlug,
|
||||||
faPlus,
|
faPlus,
|
||||||
faUserPlus,
|
faUserPlus
|
||||||
faWarning,
|
|
||||||
faXmark
|
|
||||||
} 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 { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
@ -56,7 +54,6 @@ import {
|
|||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
useCreateWorkspace,
|
useCreateWorkspace,
|
||||||
useGetUserAction,
|
|
||||||
useRegisterUserAction
|
useRegisterUserAction
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||||
@ -312,9 +309,8 @@ const LearningItem = ({
|
|||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||||
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
} mb-3 rounded-md`}
|
||||||
} mb-3 rounded-md`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
@ -325,11 +321,10 @@ const LearningItem = ({
|
|||||||
await registerUserAction.mutateAsync(userAction);
|
await registerUserAction.mutateAsync(userAction);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
|
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete
|
||||||
complete
|
|
||||||
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
? "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"
|
: "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">
|
<div className="mr-4 flex flex-row items-center">
|
||||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||||
@ -407,9 +402,8 @@ const LearningItemSquare = ({
|
|||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
||||||
complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
|
} w-full rounded-md`}
|
||||||
} w-full rounded-md`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onKeyDown={() => null}
|
onKeyDown={() => null}
|
||||||
@ -420,11 +414,10 @@ const LearningItemSquare = ({
|
|||||||
await registerUserAction.mutateAsync(userAction);
|
await registerUserAction.mutateAsync(userAction);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
|
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete
|
||||||
complete
|
|
||||||
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
|
? "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"
|
: "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-col items-center px-6 py-4">
|
||||||
<div className="flex w-full flex-row items-start justify-between">
|
<div className="flex w-full flex-row items-start justify-between">
|
||||||
@ -438,9 +431,8 @@ const LearningItemSquare = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`text-right text-sm font-normal text-mineshaft-300 ${
|
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "font-semibold text-primary" : ""
|
||||||
complete ? "font-semibold text-primary" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{complete ? "Complete!" : `About ${time}`}
|
{complete ? "Complete!" : `About ${time}`}
|
||||||
</div>
|
</div>
|
||||||
@ -483,12 +475,6 @@ const OrganizationPage = withPermission(
|
|||||||
|
|
||||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
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 { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
@ -594,31 +580,6 @@ const OrganizationPage = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
<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>
|
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||||
<div className="mt-6 flex w-full flex-row">
|
<div className="mt-6 flex w-full flex-row">
|
||||||
<Input
|
<Input
|
||||||
@ -748,95 +709,94 @@ const OrganizationPage = withPermission(
|
|||||||
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
||||||
30 * 24 * 60 * 60 * 1000
|
30 * 24 * 60 * 60 * 1000
|
||||||
) && (
|
) && (
|
||||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
<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>
|
<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">
|
<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">
|
|
||||||
<LearningItemSquare
|
<LearningItemSquare
|
||||||
text="Join Infisical Slack"
|
text="Watch Infisical demo"
|
||||||
subText="Have any questions? Ask us!"
|
subText="Set up Infisical in 3 min."
|
||||||
complete={hasUserClickedSlack}
|
complete={hasUserClickedIntro}
|
||||||
icon={faSlack}
|
icon={faHandPeace}
|
||||||
time="1 min"
|
time="3 min"
|
||||||
userAction="slack_cta_clicked"
|
userAction="intro_cta_clicked"
|
||||||
link="https://infisical.com/slack"
|
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>
|
||||||
</div>
|
{orgWorkspaces.length !== 0 && (
|
||||||
{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="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="mb-4 flex w-full flex-row items-center pr-4">
|
<div className="mr-4 flex w-full flex-row items-center">
|
||||||
<div className="mr-4 flex w-full flex-row items-center">
|
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
{false && (
|
||||||
{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">
|
||||||
<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
|
||||||
<FontAwesomeIcon
|
icon={faCheckCircle}
|
||||||
icon={faCheckCircle}
|
className="h-5 w-5 text-4xl text-green"
|
||||||
className="h-5 w-5 text-4xl text-green"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
<div className="flex flex-col items-start pl-0.5">
|
||||||
<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="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
<div className="text-sm font-normal">
|
||||||
<div className="text-sm font-normal">
|
Replace .env files with a more secure and efficient alternative.
|
||||||
Replace .env files with a more secure and efficient alternative.
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
About 2 min
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<TabsObject />
|
||||||
className={`w-28 pr-4 text-right text-sm font-semibold ${
|
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||||
false && "text-green"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
About 2 min
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<TabsObject />
|
)}
|
||||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
{orgWorkspaces.length !== 0 && (
|
||||||
</div>
|
<LearningItem
|
||||||
)}
|
text="Integrate Infisical with your infrastructure"
|
||||||
{orgWorkspaces.length !== 0 && (
|
subText="Connect Infisical to various 3rd party services and platforms."
|
||||||
<LearningItem
|
complete={false}
|
||||||
text="Integrate Infisical with your infrastructure"
|
icon={faPlug}
|
||||||
subText="Connect Infisical to various 3rd party services and platforms."
|
time="15 min"
|
||||||
complete={false}
|
link="https://infisical.com/docs/integrations/overview"
|
||||||
icon={faPlug}
|
/>
|
||||||
time="15 min"
|
)}
|
||||||
link="https://infisical.com/docs/integrations/overview"
|
</div>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
onOpenChange={(isModalOpen) => {
|
onOpenChange={(isModalOpen) => {
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
@ -61,6 +62,9 @@ import {
|
|||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useUpdateSecretV3
|
useUpdateSecretV3
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||||
|
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||||
|
import { TSecretFolder } from "@app/hooks/api/types";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
||||||
@ -87,6 +91,7 @@ export const SecretOverviewPage = () => {
|
|||||||
const parentTableRef = useRef<HTMLTableElement>(null);
|
const parentTableRef = useRef<HTMLTableElement>(null);
|
||||||
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
|
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
|
||||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parentTableRef.current) {
|
if (parentTableRef.current) {
|
||||||
@ -201,11 +206,13 @@ export const SecretOverviewPage = () => {
|
|||||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
const { mutateAsync: createFolder } = useCreateFolder();
|
const { mutateAsync: createFolder } = useCreateFolder();
|
||||||
|
const { mutateAsync: updateFolderBatch } = useUpdateFolderBatch();
|
||||||
|
|
||||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
"addSecretsInAllEnvs",
|
"addSecretsInAllEnvs",
|
||||||
"addFolder",
|
"addFolder",
|
||||||
"misc"
|
"misc",
|
||||||
|
"updateFolder"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string) => {
|
const handleFolderCreate = async (folderName: string) => {
|
||||||
@ -236,6 +243,59 @@ export const SecretOverviewPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFolderUpdate = async (newFolderName: string) => {
|
||||||
|
const { name: oldFolderName } = popUp.updateFolder.data as TSecretFolder;
|
||||||
|
|
||||||
|
const updatedFolders: TUpdateFolderBatchDTO["folders"] = [];
|
||||||
|
userAvailableEnvs.forEach((env) => {
|
||||||
|
if (
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath })
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const folder = getFolderByNameAndEnv(oldFolderName, env.slug);
|
||||||
|
if (folder) {
|
||||||
|
updatedFolders.push({
|
||||||
|
environment: env.slug,
|
||||||
|
name: newFolderName,
|
||||||
|
id: folder.id,
|
||||||
|
path: secretPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedFolders.length === 0) {
|
||||||
|
createNotification({
|
||||||
|
type: "info",
|
||||||
|
text: "You don't have access to rename selected folder"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("updateFolder");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateFolderBatch({
|
||||||
|
projectSlug,
|
||||||
|
folders: updatedFolders,
|
||||||
|
projectId: workspaceId
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully renamed folder across environments"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to rename folder across environments"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
handlePopUpClose("updateFolder");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
||||||
try {
|
try {
|
||||||
// create folder if not existing
|
// create folder if not existing
|
||||||
@ -726,6 +786,9 @@ export const SecretOverviewPage = () => {
|
|||||||
environments={visibleEnvs}
|
environments={visibleEnvs}
|
||||||
key={`overview-${folderName}-${index + 1}`}
|
key={`overview-${folderName}-${index + 1}`}
|
||||||
onClick={handleFolderClick}
|
onClick={handleFolderClick}
|
||||||
|
onToggleFolderEdit={(name: string) =>
|
||||||
|
handlePopUpOpen("updateFolder", { name })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isTableLoading &&
|
{!isTableLoading &&
|
||||||
@ -800,6 +863,18 @@ export const SecretOverviewPage = () => {
|
|||||||
<FolderForm onCreateFolder={handleFolderCreate} />
|
<FolderForm onCreateFolder={handleFolderCreate} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.updateFolder.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("updateFolder", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title="Edit Folder Name">
|
||||||
|
<FolderForm
|
||||||
|
isEdit
|
||||||
|
defaultFolderName={(popUp.updateFolder?.data as Pick<TSecretFolder, "name">)?.name}
|
||||||
|
onUpdateFolder={handleFolderUpdate}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faFolder, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Checkbox, Td, Tr } from "@app/components/v2";
|
import { Checkbox, IconButton, Td, Tr } from "@app/components/v2";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
folderName: string;
|
folderName: string;
|
||||||
@ -11,6 +11,7 @@ type Props = {
|
|||||||
onClick: (path: string) => void;
|
onClick: (path: string) => void;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onToggleFolderSelect: (folderName: string) => void;
|
onToggleFolderSelect: (folderName: string) => void;
|
||||||
|
onToggleFolderEdit: (name: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretOverviewFolderRow = ({
|
export const SecretOverviewFolderRow = ({
|
||||||
@ -19,6 +20,7 @@ export const SecretOverviewFolderRow = ({
|
|||||||
isFolderPresentInEnv,
|
isFolderPresentInEnv,
|
||||||
isSelected,
|
isSelected,
|
||||||
onToggleFolderSelect,
|
onToggleFolderSelect,
|
||||||
|
onToggleFolderEdit,
|
||||||
onClick
|
onClick
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
@ -43,6 +45,18 @@ export const SecretOverviewFolderRow = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>{folderName}</div>
|
<div>{folderName}</div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="edit-folder"
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className="p-0 opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
onToggleFolderEdit(folderName);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} size="sm" />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
{environments.map(({ slug }, i) => {
|
{environments.map(({ slug }, i) => {
|
||||||
|
Reference in New Issue
Block a user