mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Ghost user and migration finished!
This commit is contained in:
126
backend/src/lib/secret/index.ts
Normal file
126
backend/src/lib/secret/index.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding, TProjectKeys } from "@app/db/schemas";
|
||||||
|
|
||||||
|
import { decryptAsymmetric, decryptSymmetric } from "../crypto";
|
||||||
|
import { decryptSymmetric128BitHexKeyUTF8, TDecryptSymmetricInput } from "../crypto/encryption";
|
||||||
|
|
||||||
|
export enum SecretDocType {
|
||||||
|
Secret = "secret",
|
||||||
|
SecretVersion = "secretVersion",
|
||||||
|
ApprovalSecret = "approvalSecret"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PartialSecretSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
secretKeyCiphertext: z.string(),
|
||||||
|
secretKeyIV: z.string(),
|
||||||
|
secretKeyTag: z.string(),
|
||||||
|
|
||||||
|
secretValueCiphertext: z.string(),
|
||||||
|
secretValueIV: z.string(),
|
||||||
|
secretValueTag: z.string(),
|
||||||
|
|
||||||
|
secretCommentCiphertext: z.string().nullish(),
|
||||||
|
secretCommentIV: z.string().nullish(),
|
||||||
|
secretCommentTag: z.string().nullish(),
|
||||||
|
|
||||||
|
docType: z.nativeEnum(SecretDocType),
|
||||||
|
|
||||||
|
keyEncoding: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const PartialDecryptedSecretSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
secretKey: z.string(),
|
||||||
|
secretValue: z.string(),
|
||||||
|
secretComment: z.string().optional(),
|
||||||
|
|
||||||
|
docType: z.nativeEnum(SecretDocType)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TPartialSecret = z.infer<typeof PartialSecretSchema>;
|
||||||
|
export type TPartialDecryptedSecret = z.infer<typeof PartialDecryptedSecretSchema>;
|
||||||
|
|
||||||
|
const symmetricDecrypt = ({
|
||||||
|
keyEncoding,
|
||||||
|
ciphertext,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
key,
|
||||||
|
isApprovalSecret
|
||||||
|
}: TDecryptSymmetricInput & { keyEncoding: SecretKeyEncoding; isApprovalSecret: boolean }) => {
|
||||||
|
if (keyEncoding === SecretKeyEncoding.UTF8 || isApprovalSecret) {
|
||||||
|
const data = decryptSymmetric128BitHexKeyUTF8({ key, iv, tag, ciphertext });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (keyEncoding === SecretKeyEncoding.BASE64) {
|
||||||
|
const data = decryptSymmetric({ key, iv, tag, ciphertext });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Missing both encryption keys");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decryptSecrets = (
|
||||||
|
encryptedSecrets: TPartialSecret[],
|
||||||
|
privateKey: string,
|
||||||
|
latestKey: TProjectKeys & {
|
||||||
|
sender: {
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const key = decryptAsymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const secrets: TPartialDecryptedSecret[] = [];
|
||||||
|
|
||||||
|
encryptedSecrets.forEach((encSecret) => {
|
||||||
|
const secretKey = symmetricDecrypt({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key,
|
||||||
|
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
|
||||||
|
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = symmetricDecrypt({
|
||||||
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
|
iv: encSecret.secretValueIV,
|
||||||
|
tag: encSecret.secretValueTag,
|
||||||
|
key,
|
||||||
|
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
|
||||||
|
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretComment =
|
||||||
|
encSecret.secretCommentCiphertext && encSecret.secretCommentIV && encSecret.secretCommentTag
|
||||||
|
? symmetricDecrypt({
|
||||||
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
|
iv: encSecret.secretCommentIV,
|
||||||
|
tag: encSecret.secretCommentTag,
|
||||||
|
key,
|
||||||
|
keyEncoding: encSecret.keyEncoding as SecretKeyEncoding,
|
||||||
|
isApprovalSecret: encSecret.docType === SecretDocType.ApprovalSecret
|
||||||
|
})
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const decryptedSecret: TPartialDecryptedSecret = {
|
||||||
|
id: encSecret.id,
|
||||||
|
secretKey,
|
||||||
|
secretValue,
|
||||||
|
secretComment,
|
||||||
|
docType: encSecret.docType
|
||||||
|
};
|
||||||
|
|
||||||
|
secrets.push(decryptedSecret);
|
||||||
|
});
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
@@ -327,7 +327,12 @@ export const registerRoutes = async (
|
|||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
|
secretDAL,
|
||||||
|
orgDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL: sarSecretDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
|
secretVersionDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
orgService,
|
orgService,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectId: req.params.projectId
|
projectId: req.params.projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!project.e2ee) {
|
if (project.version === "v2") {
|
||||||
throw new BadRequestError({ message: "Failed to find bot, project has E2EE disabled" });
|
throw new BadRequestError({ message: "Failed to find bot, project has E2EE disabled" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ export const registerProjectBotRouter = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const project = await server.services.projectBot.findProjectByBotId(req.params.botId);
|
const project = await server.services.projectBot.findProjectByBotId(req.params.botId);
|
||||||
|
|
||||||
if (project?.e2ee === false) {
|
if (project?.version === "v2") {
|
||||||
throw new BadRequestError({ message: "Failed to set bot active, project has E2EE disabled" });
|
throw new BadRequestError({ message: "Failed to set bot active, project has E2EE disabled" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const projectWithEnv = ProjectsSchema.merge(
|
|||||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
/* Get project key */
|
/* Get project key */
|
||||||
server.route({
|
server.route({
|
||||||
url: "/:workspaceId/encrypted-key",
|
url: "/:projectId/encrypted-key",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
description: "Return encrypted project key",
|
description: "Return encrypted project key",
|
||||||
@@ -27,7 +27,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
workspaceId: z.string().trim()
|
projectId: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: ProjectKeysSchema.merge(
|
200: ProjectKeysSchema.merge(
|
||||||
@@ -50,7 +50,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.params.workspaceId,
|
projectId: req.params.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_WORKSPACE_KEY,
|
type: EventType.GET_WORKSPACE_KEY,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { registerLoginRouter } from "./login-router";
|
import { registerLoginRouter } from "./login-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
|
||||||
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
import { registerSecretBlindIndexRouter } from "./secret-blind-index-router";
|
||||||
import { registerSecretRouter } from "./secret-router";
|
import { registerSecretRouter } from "./secret-router";
|
||||||
import { registerSignupRouter } from "./signup-router";
|
import { registerSignupRouter } from "./signup-router";
|
||||||
@@ -11,5 +10,4 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
await server.register(registerSecretRouter, { prefix: "/secrets" });
|
||||||
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
|
||||||
await server.register(registerProjectRouter, { prefix: "/projects" });
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { ProjectMembershipsSchema, ProjectsSchema } from "@app/db/schemas";
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
|
||||||
|
|
||||||
const projectWithEnv = ProjectsSchema.merge(
|
|
||||||
z.object({
|
|
||||||
_id: z.string(),
|
|
||||||
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|
||||||
/* Create new project */
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/",
|
|
||||||
config: {
|
|
||||||
rateLimit: authRateLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
body: z.object({
|
|
||||||
projectName: z.string().trim(),
|
|
||||||
organizationId: z.string().trim()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
project: projectWithEnv
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handler: async (req) => {
|
|
||||||
const project = await server.services.project.createProject({
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
orgId: req.body.organizationId,
|
|
||||||
workspaceName: req.body.projectName
|
|
||||||
});
|
|
||||||
|
|
||||||
return { project };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/:projectId/memberships",
|
|
||||||
config: {
|
|
||||||
rateLimit: authRateLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
params: z.object({
|
|
||||||
projectId: z.string()
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
emails: z.string().email().array()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
memberships: ProjectMembershipsSchema.array()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handler: async (req) => {
|
|
||||||
const memberships = await server.services.projectMembership.addUsersToProjectNonE2EE({
|
|
||||||
projectId: req.params.projectId,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actor: req.permission.type,
|
|
||||||
emails: req.body.emails
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
|
||||||
projectId: req.params.projectId,
|
|
||||||
...req.auditLogInfo,
|
|
||||||
event: {
|
|
||||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
|
|
||||||
metadata: memberships.map(({ userId, id }) => ({
|
|
||||||
userId: userId || "",
|
|
||||||
membershipId: id,
|
|
||||||
email: ""
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { memberships };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
@@ -6,10 +8,18 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
|||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
|
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||||
|
import { RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import {
|
||||||
|
decryptAsymmetric,
|
||||||
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
|
infisicalSymmetricDecrypt,
|
||||||
|
infisicalSymmetricEncypt
|
||||||
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
@@ -17,11 +27,14 @@ import { TProjectPermission } from "@app/lib/types";
|
|||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
@@ -65,13 +78,18 @@ export const projectServiceFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
projectQueue,
|
projectQueue,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretApprovalSecretDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
userDAL,
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
orgService,
|
orgService,
|
||||||
|
orgDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
|
secretVersionDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
|
secretDAL,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -14,6 +14,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
Button,
|
Button,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -38,7 +40,8 @@ import {
|
|||||||
useGetFoldersByEnv,
|
useGetFoldersByEnv,
|
||||||
useGetProjectSecretsAllEnv,
|
useGetProjectSecretsAllEnv,
|
||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useUpdateSecretV3
|
useUpdateSecretV3,
|
||||||
|
useUpgradeProject
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
@@ -106,6 +109,7 @@ export const SecretOverviewPage = () => {
|
|||||||
environments: userAvailableEnvs.map(({ slug }) => slug)
|
environments: userAvailableEnvs.map(({ slug }) => slug)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const upgradeProject = useUpgradeProject();
|
||||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
@@ -199,6 +203,24 @@ export const SecretOverviewPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUpgradeProject = useCallback(async () => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Private key not found"
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await upgradeProject.mutateAsync({
|
||||||
|
projectId: workspaceId,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleResetSearch = () => setSearchFilter("");
|
const handleResetSearch = () => setSearchFilter("");
|
||||||
|
|
||||||
const handleFolderClick = (path: string) => {
|
const handleFolderClick = (path: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user