From 4802a36473f23ae294e076a8680b9cc8150eb4ed Mon Sep 17 00:00:00 2001
From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com>
Date: Tue, 13 Feb 2024 01:03:59 +0100
Subject: [PATCH] Finished migration
---
.../migrations/20240202093209_ghost_user.ts | 2 +
backend/src/db/schemas/models.ts | 18 +
backend/src/db/schemas/projects.ts | 3 +-
.../saml-config/saml-config-service.ts | 3 +-
backend/src/queue/queue-service.ts | 21 +-
backend/src/server/routes/index.ts | 25 +-
.../src/server/routes/v2/project-router.ts | 27 +-
.../src/services/auth/auth-signup-service.ts | 2 +-
backend/src/services/project/project-dal.ts | 15 +-
backend/src/services/project/project-queue.ts | 410 ++++++++++++++++++
.../src/services/project/project-service.ts | 348 ++-------------
.../super-admin/super-admin-service.ts | 1 +
.../UpgradeProjectAlert.tsx | 90 ++++
.../v2/UpgradeProjectAlert/index.tsx | 1 +
frontend/src/hooks/api/workspace/index.tsx | 1 +
frontend/src/hooks/api/workspace/queries.tsx | 17 +
frontend/src/hooks/api/workspace/types.ts | 1 +
.../SecretOverviewPage/SecretOverviewPage.tsx | 44 +-
18 files changed, 660 insertions(+), 369 deletions(-)
create mode 100644 backend/src/services/project/project-queue.ts
create mode 100644 frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx
create mode 100644 frontend/src/components/v2/UpgradeProjectAlert/index.tsx
diff --git a/backend/src/db/migrations/20240202093209_ghost_user.ts b/backend/src/db/migrations/20240202093209_ghost_user.ts
index 72181cd83..ddfdbe812 100644
--- a/backend/src/db/migrations/20240202093209_ghost_user.ts
+++ b/backend/src/db/migrations/20240202093209_ghost_user.ts
@@ -15,6 +15,7 @@ export async function up(knex: Knex): Promise {
if (!hasProjectVersionColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("version").defaultTo(ProjectVersion.V1).notNullable();
+ t.text("upgradeStatus").nullable();
});
}
}
@@ -32,6 +33,7 @@ export async function down(knex: Knex): Promise {
if (hasProjectVersionColumn) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("version");
+ t.dropColumn("upgradeStatus");
});
}
}
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index 4109c516c..426d3af3f 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -117,6 +117,24 @@ export enum ProjectVersion {
V2 = "v2"
}
+// Case study:
+/*
+
+Q: If the upgrade status is null and the project version is V1:
+A: It would mean that the project has not been attempted to be upgraded
+
+Q: If the upgrade status is not null (FAILED or IN_PROGRESS), we a status
+
+Q: if the project status is null, and the project version is v2, we should display nothing cuz its upgraded!
+
+*/
+
+export enum ProjectUpgradeStatus {
+ InProgress = "IN_PROGRESS",
+ Completed = "Project upgrade completed.", // Will be null if completed. So a completed status is not needed
+ Failed = "FAILED"
+}
+
export enum IdentityAuthMethod {
Univeral = "universal-auth"
}
diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts
index fceb18388..92be7a651 100644
--- a/backend/src/db/schemas/projects.ts
+++ b/backend/src/db/schemas/projects.ts
@@ -15,7 +15,8 @@ export const ProjectsSchema = z.object({
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
- version: z.string().default("v1")
+ version: z.string().default("v1"),
+ upgradeStatus: z.string().nullable().optional()
});
export type TProjects = z.infer;
diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts
index ad33051fb..824db8251 100644
--- a/backend/src/ee/services/saml-config/saml-config-service.ts
+++ b/backend/src/ee/services/saml-config/saml-config-service.ts
@@ -338,7 +338,8 @@ export const samlConfigServiceFactory = ({
email,
firstName,
lastName,
- authMethods: [AuthMethod.EMAIL]
+ authMethods: [AuthMethod.EMAIL],
+ ghost: false
},
tx
);
diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts
index 58c829549..e11e0b590 100644
--- a/backend/src/queue/queue-service.ts
+++ b/backend/src/queue/queue-service.ts
@@ -1,6 +1,7 @@
import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis";
+import { SecretKeyEncoding } from "@app/db/schemas";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import {
TScanFullRepoEventPayload,
@@ -15,7 +16,8 @@ export enum QueueName {
IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook",
SecretFullRepoScan = "secret-full-repo-scan",
- SecretPushEventScan = "secret-push-event-scan"
+ SecretPushEventScan = "secret-push-event-scan",
+ UpgradeProjectToGhost = "upgrade-project-to-ghost"
}
export enum QueueJobs {
@@ -25,7 +27,8 @@ export enum QueueJobs {
AuditLogPrune = "audit-log-prune-job",
SecWebhook = "secret-webhook-trigger",
IntegrationSync = "secret-integration-pull",
- SecretScan = "secret-scan"
+ SecretScan = "secret-scan",
+ UpgradeProjectToGhost = "upgrade-project-to-ghost-job"
}
export type TQueueJobTypes = {
@@ -64,6 +67,20 @@ export type TQueueJobTypes = {
payload: TScanFullRepoEventPayload;
};
[QueueName.SecretPushEventScan]: { name: QueueJobs.SecretScan; payload: TScanPushEventPayload };
+
+ [QueueName.UpgradeProjectToGhost]: {
+ name: QueueJobs.UpgradeProjectToGhost;
+ payload: {
+ projectId: string;
+ startedByUserId: string;
+ encryptedPrivateKey: {
+ encryptedKey: string;
+ encryptedKeyIv: string;
+ encryptedKeyTag: string;
+ keyEncoding: SecretKeyEncoding;
+ };
+ };
+ };
};
export type TQueueServiceFactory = ReturnType;
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 068142ab8..3952ea980 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -65,6 +65,7 @@ import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { projectDALFactory } from "@app/services/project/project-dal";
+import { projectQueueFactory } from "@app/services/project/project-queue";
import { projectServiceFactory } from "@app/services/project/project-service";
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -299,19 +300,33 @@ export const registerRoutes = async (
projectKeyDAL,
projectMembershipDAL
});
+
+ const projectQueueService = projectQueueFactory({
+ queueService,
+ secretDAL,
+ folderDAL,
+ projectDAL,
+ orgDAL,
+ orgService,
+ projectEnvDAL,
+ userDAL,
+ secretVersionDAL,
+ projectKeyDAL,
+ projectBotDAL,
+ projectMembershipDAL,
+ secretApprovalRequestDAL,
+ secretApprovalSecretDAL: sarSecretDAL
+ });
+
const projectService = projectServiceFactory({
permissionService,
projectDAL,
+ projectQueue: projectQueueService,
secretBlindIndexDAL,
identityProjectDAL,
identityOrgMembershipDAL,
projectBotDAL,
- secretDAL,
- orgDAL,
- secretApprovalRequestDAL,
- secretApprovalSecretDAL: sarSecretDAL,
projectKeyDAL,
- secretVersionDAL,
userDAL,
projectEnvDAL,
orgService,
diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts
index bc0282850..fd7573c65 100644
--- a/backend/src/server/routes/v2/project-router.ts
+++ b/backend/src/server/routes/v2/project-router.ts
@@ -74,7 +74,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
userPrivateKey: z.string().trim()
}),
response: {
- 200: z.object({})
+ 200: z.void()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
@@ -88,6 +88,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
+ server.route({
+ url: "/:projectId/upgrade/status",
+ method: "GET",
+ schema: {
+ params: z.object({
+ projectId: z.string().trim()
+ }),
+ response: {
+ 200: z.object({
+ status: z.string().nullable()
+ })
+ }
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
+ handler: async (req) => {
+ const status = await server.services.project.getProjectUpgradeStatus({
+ projectId: req.params.projectId,
+ actor: req.permission.type,
+ actorId: req.permission.id
+ });
+
+ return { status };
+ }
+ });
+
/* Create new project */
server.route({
method: "POST",
diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts
index 021ca7070..0c2f53e5f 100644
--- a/backend/src/services/auth/auth-signup-service.ts
+++ b/backend/src/services/auth/auth-signup-service.ts
@@ -50,7 +50,7 @@ export const authSignupServiceFactory = ({
throw new Error("Failed to send verification code for complete account");
}
if (!user) {
- user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email });
+ user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, ghost: false });
}
if (!user) throw new Error("Failed to create user");
diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts
index 4d7c6072e..a9906f859 100644
--- a/backend/src/services/project/project-dal.ts
+++ b/backend/src/services/project/project-dal.ts
@@ -1,5 +1,5 @@
import { TDbClient } from "@app/db";
-import { ProjectsSchema, TableName } from "@app/db/schemas";
+import { ProjectsSchema, ProjectUpgradeStatus, TableName, TProjectsUpdate } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@@ -66,6 +66,18 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
+ const setProjectUpgradeStatus = async (projectId: string, status: ProjectUpgradeStatus) => {
+ try {
+ const data: TProjectsUpdate = {
+ upgradeStatus: status
+ } as const;
+
+ await db(TableName.Project).where({ id: projectId }).update(data);
+ } catch (error) {
+ throw new DatabaseError({ error, name: "Set project upgrade status" });
+ }
+ };
+
const findAllProjectsByIdentity = async (identityId: string) => {
try {
const workspaces = await db(TableName.IdentityProjectMembership)
@@ -149,6 +161,7 @@ export const projectDALFactory = (db: TDbClient) => {
return {
...projectOrm,
findAllProjects,
+ setProjectUpgradeStatus,
findAllProjectsByIdentity,
findProjectGhostUser,
findProjectById
diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts
new file mode 100644
index 000000000..e8b15ddf9
--- /dev/null
+++ b/backend/src/services/project/project-queue.ts
@@ -0,0 +1,410 @@
+/* eslint-disable no-await-in-loop */
+import {
+ ProjectMembershipRole,
+ ProjectUpgradeStatus,
+ ProjectVersion,
+ SecretKeyEncoding,
+ TSecrets
+} from "@app/db/schemas";
+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 {
+ decryptAsymmetric,
+ encryptSymmetric128BitHexKeyUTF8,
+ infisicalSymmetricDecrypt,
+ infisicalSymmetricEncypt
+} from "@app/lib/crypto/encryption";
+import { BadRequestError } from "@app/lib/errors";
+import { logger } from "@app/lib/logger";
+import { createProjectKey, createWsMembers } from "@app/lib/project";
+import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret";
+import { QueueJobs, QueueName, TQueueJobTypes, TQueueServiceFactory } from "@app/queue";
+
+import { TOrgDALFactory } from "../org/org-dal";
+import { TOrgServiceFactory } from "../org/org-service";
+import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
+import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
+import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
+import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
+import { TSecretDALFactory } from "../secret/secret-dal";
+import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
+import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
+import { TUserDALFactory } from "../user/user-dal";
+import { TProjectDALFactory } from "./project-dal";
+
+export type TProjectQueueFactory = ReturnType;
+
+type TProjectQueueFactoryDep = {
+ queueService: TQueueServiceFactory;
+ secretVersionDAL: Pick;
+ folderDAL: Pick;
+ secretDAL: Pick;
+ projectKeyDAL: Pick;
+ secretApprovalRequestDAL: Pick;
+ secretApprovalSecretDAL: Pick;
+ projectBotDAL: Pick;
+ orgService: Pick;
+ projectMembershipDAL: Pick;
+ userDAL: Pick;
+
+ projectEnvDAL: Pick;
+ projectDAL: Pick;
+ orgDAL: Pick;
+};
+
+export const projectQueueFactory = ({
+ queueService,
+ secretDAL,
+ folderDAL,
+ userDAL,
+ secretVersionDAL,
+ secretApprovalRequestDAL,
+ secretApprovalSecretDAL,
+ projectKeyDAL,
+ projectBotDAL,
+ projectEnvDAL,
+ orgDAL,
+ projectDAL,
+ orgService,
+ projectMembershipDAL
+}: TProjectQueueFactoryDep) => {
+ const upgradeProject = async (dto: TQueueJobTypes["upgrade-project-to-ghost"]["payload"]) => {
+ await queueService.queue(QueueName.UpgradeProjectToGhost, QueueJobs.UpgradeProjectToGhost, dto, {
+ attempts: 1,
+ removeOnComplete: true,
+ removeOnFail: {
+ count: 5 // keep the most recent jobs
+ }
+ });
+ };
+
+ queueService.start(QueueName.UpgradeProjectToGhost, async ({ data }) => {
+ try {
+ const [project] = await projectDAL.find({
+ id: data.projectId,
+ version: ProjectVersion.V1,
+ $in: {
+ upgradeStatus: [ProjectUpgradeStatus.Failed, null]
+ }
+ });
+
+ const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId);
+
+ if (!project || !oldProjectKey) {
+ throw new BadRequestError({
+ message: "Project or project key not found"
+ });
+ }
+
+ await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.InProgress);
+ const userPrivateKey = infisicalSymmetricDecrypt({
+ keyEncoding: data.encryptedPrivateKey.keyEncoding,
+ ciphertext: data.encryptedPrivateKey.encryptedKey,
+ iv: data.encryptedPrivateKey.encryptedKeyIv,
+ tag: data.encryptedPrivateKey.encryptedKeyTag
+ });
+
+ const projectEnvs = await projectEnvDAL.find({
+ projectId: project.id
+ });
+
+ const projectFolders = await folderDAL.find({
+ $in: {
+ envId: projectEnvs.map((env) => env.id)
+ }
+ });
+
+ // Get all the secrets within the project (as encrypted)
+ const secrets: TPartialSecret[] = [];
+ for (const folder of projectFolders) {
+ const folderSecrets = await secretDAL.find({ folderId: folder.id });
+
+ const folderSecretVersions = await secretVersionDAL.find(
+ {
+ folderId: folder.id
+ },
+ // Only get the latest 100 secret versions for each folder.
+ {
+ limit: 100
+ }
+ );
+ const approvalRequests = await secretApprovalRequestDAL.find({
+ status: RequestState.Open,
+ folderId: folder.id
+ });
+ const approvalSecrets = await secretApprovalSecretDAL.find({
+ $in: {
+ requestId: approvalRequests.map((el) => el.id)
+ }
+ });
+
+ secrets.push(...folderSecrets.map((el) => ({ ...el, docType: SecretDocType.Secret })));
+ secrets.push(...folderSecretVersions.map((el) => ({ ...el, docType: SecretDocType.SecretVersion })));
+ secrets.push(...approvalSecrets.map((el) => ({ ...el, docType: SecretDocType.ApprovalSecret })));
+ }
+
+ const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
+
+ if (secrets.length !== decryptedSecrets.length) {
+ throw new Error("Failed to decrypt some secret versions");
+ }
+
+ // Get the existing bot and the existing project keys for the members of the project
+ const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
+ const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
+
+ // TRANSACTION START
+ await projectDAL.transaction(async (tx) => {
+ await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
+
+ // Create a ghost user
+ const ghostUser = await orgService.addGhostUser(project.orgId, tx);
+
+ // Create a project key
+ const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
+ publicKey: ghostUser.keys.publicKey,
+ privateKey: ghostUser.keys.plainPrivateKey
+ });
+
+ // Create a new project key for the GHOST
+ await projectKeyDAL.create(
+ {
+ projectId: project.id,
+ receiverId: ghostUser.user.id,
+ encryptedKey: newEncryptedProjectKey,
+ nonce: newEncryptedProjectKeyIv,
+ senderId: ghostUser.user.id
+ },
+ tx
+ );
+
+ // Create a membership for the ghost user
+ await projectMembershipDAL.create(
+ {
+ projectId: project.id,
+ userId: ghostUser.user.id,
+ role: ProjectMembershipRole.Admin
+ },
+ tx
+ );
+
+ // If a bot already exists, delete it
+ if (existingBot) {
+ await projectBotDAL.delete({ id: existingBot.id }, tx);
+ }
+
+ // Delete all the existing project keys
+ await projectKeyDAL.delete(
+ {
+ projectId: project.id,
+ $in: {
+ id: existingProjectKeys.map((key) => key.id)
+ }
+ },
+ tx
+ );
+
+ const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
+
+ if (!ghostUserLatestKey) {
+ throw new Error("User latest key not found (V2 Upgrade)");
+ }
+
+ const newProjectMembers: {
+ encryptedKey: string;
+ nonce: string;
+ senderId: string;
+ receiverId: string;
+ projectId: string;
+ }[] = [];
+
+ for (const key of existingProjectKeys) {
+ const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
+ const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
+
+ if (!user || !orgMembership) {
+ throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
+ }
+
+ const [newMember] = createWsMembers({
+ decryptKey: ghostUserLatestKey,
+ userPrivateKey: ghostUser.keys.plainPrivateKey,
+ members: [
+ {
+ userPublicKey: user.publicKey,
+ orgMembershipId: orgMembership.id,
+ projectMembershipRole: ProjectMembershipRole.Admin
+ }
+ ]
+ });
+
+ newProjectMembers.push({
+ encryptedKey: newMember.workspaceEncryptedKey,
+ nonce: newMember.workspaceEncryptedNonce,
+ senderId: ghostUser.user.id,
+ receiverId: user.id,
+ projectId: project.id
+ });
+ }
+
+ // Create project keys for all the old members
+ await projectKeyDAL.insertMany(newProjectMembers, tx);
+
+ // Encrypt the bot private key (which is the same as the ghost user)
+ const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
+
+ // 5. Create a bot for the project
+ const newBot = await projectBotDAL.create(
+ {
+ name: "Infisical Bot (Ghost)",
+ projectId: project.id,
+ tag,
+ iv,
+ encryptedPrivateKey: ciphertext,
+ isActive: true,
+ publicKey: ghostUser.keys.publicKey,
+ senderId: ghostUser.user.id,
+ encryptedProjectKey: newEncryptedProjectKey,
+ encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
+ algorithm,
+ keyEncoding: encoding
+ },
+ tx
+ );
+
+ const botPrivateKey = infisicalSymmetricDecrypt({
+ keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
+ iv: newBot.iv,
+ tag: newBot.tag,
+ ciphertext: newBot.encryptedPrivateKey
+ });
+
+ const botKey = decryptAsymmetric({
+ ciphertext: newBot.encryptedProjectKey!,
+ privateKey: botPrivateKey,
+ nonce: newBot.encryptedProjectKeyNonce!,
+ publicKey: ghostUser.keys.publicKey
+ });
+
+ type TPartialSecret = Pick<
+ TSecrets,
+ | "id"
+ | "secretKeyCiphertext"
+ | "secretKeyIV"
+ | "secretKeyTag"
+ | "secretValueCiphertext"
+ | "secretValueIV"
+ | "secretValueTag"
+ | "secretCommentCiphertext"
+ | "secretCommentIV"
+ | "secretCommentTag"
+ >;
+
+ const updatedSecrets: TPartialSecret[] = [];
+ const updatedSecretVersions: TPartialSecret[] = [];
+ const updatedSecretApprovals: TPartialSecret[] = [];
+ for (const rawSecret of decryptedSecrets) {
+ const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretKey, botKey);
+ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretValue || "", botKey);
+ const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretComment || "", botKey);
+
+ const payload = {
+ id: rawSecret.id,
+ secretKeyCiphertext: secretKeyEncrypted.ciphertext,
+ secretKeyIV: secretKeyEncrypted.iv,
+ secretKeyTag: secretKeyEncrypted.tag,
+ secretValueCiphertext: secretValueEncrypted.ciphertext,
+ secretValueIV: secretValueEncrypted.iv,
+ secretValueTag: secretValueEncrypted.tag,
+ secretCommentCiphertext: secretCommentEncrypted.ciphertext,
+ secretCommentIV: secretCommentEncrypted.iv,
+ secretCommentTag: secretCommentEncrypted.tag
+ } as const;
+
+ if (rawSecret.docType === SecretDocType.Secret) {
+ updatedSecrets.push(payload);
+ } else if (rawSecret.docType === SecretDocType.SecretVersion) {
+ updatedSecretVersions.push(payload);
+ } else if (rawSecret.docType === SecretDocType.ApprovalSecret) {
+ updatedSecretApprovals.push(payload);
+ } else {
+ throw new Error("Unknown secret type");
+ }
+ }
+
+ const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(
+ [
+ ...updatedSecrets.map((secret) => ({
+ filter: { id: secret.id },
+ data: {
+ ...secret,
+ id: undefined
+ }
+ }))
+ ],
+ tx
+ );
+
+ const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(
+ [
+ ...updatedSecretVersions.map((version) => ({
+ filter: { id: version.id },
+ data: {
+ ...version,
+ id: undefined
+ }
+ }))
+ ],
+ tx
+ );
+
+ const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
+ [
+ ...updatedSecretApprovals.map((approval) => ({
+ filter: {
+ id: approval.id
+ },
+ data: {
+ ...approval,
+ id: undefined
+ }
+ }))
+ ],
+ tx
+ );
+
+ if (secretUpdates.length !== updatedSecrets.length) {
+ throw new Error("Failed to update some secrets");
+ }
+ if (secretVersionUpdates.length !== updatedSecretVersions.length) {
+ throw new Error("Failed to update some secret versions");
+ }
+ if (secretApprovalUpdates.length !== updatedSecretApprovals.length) {
+ throw new Error("Failed to update some secret approvals");
+ }
+
+ throw new Error("Transaction was successful!");
+ });
+ } catch (err) {
+ const project = await projectDAL.findOne({ id: data.projectId, version: ProjectVersion.V1 }).catch(() => null);
+
+ if (!project) {
+ logger.error("Failed to upgrade project, because no project was found", data);
+ } else {
+ await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
+ logger.error("Failed to upgrade project", data, err);
+ }
+
+ throw err;
+ }
+ });
+
+ queueService.listen(QueueName.UpgradeProjectToGhost, "failed", (job, err) => {
+ logger.error("Upgrade project failed", job?.data, err);
+ });
+
+ return {
+ upgradeProject
+ };
+};
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index 9b079576b..8c763d0db 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -1,45 +1,36 @@
+/* eslint-disable @typescript-eslint/no-redeclare */
/* eslint-disable no-console */
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
-import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TSecrets } from "@app/db/schemas";
+import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
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 { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
-import {
- decryptAsymmetric,
- encryptSymmetric128BitHexKeyUTF8,
- infisicalSymmetricDecrypt,
- infisicalSymmetricEncypt
-} from "@app/lib/crypto/encryption";
+import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { createProjectKey, createWsMembers } from "@app/lib/project";
-import { decryptSecrets, SecretDocType, TPartialSecret } from "@app/lib/secret";
+import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal";
-import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-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 { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal";
+import { TProjectQueueFactory } from "./project-queue";
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO, TUpgradeProjectDTO } from "./project-types";
export const DEFAULT_PROJECT_ENVS = [
@@ -50,22 +41,18 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory;
+ projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory;
folderDAL: TSecretFolderDALFactory;
projectEnvDAL: Pick;
- secretVersionDAL: TSecretVersionDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
projectKeyDAL: Pick;
projectBotDAL: Pick;
projectMembershipDAL: Pick;
- orgDAL: TOrgDALFactory;
- secretApprovalRequestDAL: TSecretApprovalRequestDALFactory;
- secretApprovalSecretDAL: TSecretApprovalRequestSecretDALFactory;
secretBlindIndexDAL: Pick;
permissionService: TPermissionServiceFactory;
orgService: Pick;
- secretDAL: TSecretDALFactory;
licenseService: Pick;
};
@@ -73,19 +60,15 @@ export type TProjectServiceFactory = ReturnType;
export const projectServiceFactory = ({
projectDAL,
+ projectQueue,
projectKeyDAL,
- secretApprovalRequestDAL,
- secretApprovalSecretDAL,
permissionService,
userDAL,
folderDAL,
orgService,
- orgDAL,
identityProjectDAL,
- secretVersionDAL,
projectBotDAL,
identityOrgMembershipDAL,
- secretDAL,
secretBlindIndexDAL,
projectMembershipDAL,
projectEnvDAL,
@@ -396,303 +379,33 @@ export const projectServiceFactory = ({
25. API route returns 200 OK.
*/
- const project = await projectDAL.findOne({ id: projectId, version: ProjectVersion.V1 });
- const oldProjectKey = await projectKeyDAL.findLatestProjectKey(actorId, projectId);
- if (!project || !oldProjectKey) {
+ const encryptedPrivateKey = infisicalSymmetricEncypt(userPrivateKey);
+
+ await projectQueue.upgradeProject({
+ projectId,
+ startedByUserId: actorId,
+ encryptedPrivateKey: {
+ encryptedKey: encryptedPrivateKey.ciphertext,
+ encryptedKeyIv: encryptedPrivateKey.iv,
+ encryptedKeyTag: encryptedPrivateKey.tag,
+ keyEncoding: encryptedPrivateKey.encoding
+ }
+ });
+ };
+
+ const getProjectUpgradeStatus = async ({ projectId, actor, actorId }: TProjectPermission) => {
+ const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
+ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
+
+ const project = await projectDAL.findProjectById(projectId);
+
+ if (!project) {
throw new BadRequestError({
- message: "Project or project key not found"
+ message: `Project with id ${projectId} not found`
});
}
- const projectEnvs = await projectEnvDAL.find({
- projectId: project.id
- });
-
- console.log(
- "projectEnvs",
- projectEnvs.map((e) => e.name)
- );
-
- const projectFolders = await folderDAL.find({
- $in: {
- envId: projectEnvs.map((env) => env.id)
- }
- });
-
- // Get all the secrets within the project (as encrypted)
- const secrets: TPartialSecret[] = [];
- for (const folder of projectFolders) {
- const folderSecrets = await secretDAL.find({ folderId: folder.id });
-
- const folderSecretVersions = await secretVersionDAL.find({
- folderId: folder.id
- });
- const approvalRequests = await secretApprovalRequestDAL.find({
- status: RequestState.Open,
- folderId: folder.id
- });
- const approvalSecrets = await secretApprovalSecretDAL.find({
- $in: {
- requestId: approvalRequests.map((el) => el.id)
- }
- });
-
- secrets.push(...folderSecrets.map((el) => ({ ...el, docType: SecretDocType.Secret })));
- secrets.push(...folderSecretVersions.map((el) => ({ ...el, docType: SecretDocType.SecretVersion })));
- secrets.push(...approvalSecrets.map((el) => ({ ...el, docType: SecretDocType.ApprovalSecret })));
- }
-
- const decryptedSecrets = decryptSecrets(secrets, userPrivateKey, oldProjectKey);
-
- if (secrets.length !== decryptedSecrets.length) {
- throw new Error("Failed to decrypt some secret versions");
- }
-
- // Get the existing bot and the existing project keys for the members of the project
- const existingBot = await projectBotDAL.findOne({ projectId: project.id }).catch(() => null);
- const existingProjectKeys = await projectKeyDAL.find({ projectId: project.id });
-
- // TRANSACTION START
- await projectDAL.transaction(async (tx) => {
- await projectDAL.updateById(project.id, { version: ProjectVersion.V2 }, tx);
-
- // Create a ghost user
- const ghostUser = await orgService.addGhostUser(project.orgId, tx);
-
- // Create a project key
- const { key: newEncryptedProjectKey, iv: newEncryptedProjectKeyIv } = createProjectKey({
- publicKey: ghostUser.keys.publicKey,
- privateKey: ghostUser.keys.plainPrivateKey
- });
-
- console.log("Creating new project key for ghost user");
- // Create a new project key for the GHOST
- await projectKeyDAL.create(
- {
- projectId: project.id,
- receiverId: ghostUser.user.id,
- encryptedKey: newEncryptedProjectKey,
- nonce: newEncryptedProjectKeyIv,
- senderId: ghostUser.user.id
- },
- tx
- );
-
- // Create a membership for the ghost user
- await projectMembershipDAL.create(
- {
- projectId: project.id,
- userId: ghostUser.user.id,
- role: ProjectMembershipRole.Admin
- },
- tx
- );
-
- // If a bot already exists, delete it
- if (existingBot) {
- console.log("Deleting existing bot");
- await projectBotDAL.delete({ id: existingBot.id }, tx);
- }
-
- console.log("Deleting old project keys");
- // Delete all the existing project keys
- await projectKeyDAL.delete(
- {
- projectId: project.id,
- $in: {
- id: existingProjectKeys.map((key) => key.id)
- }
- },
- tx
- );
-
- console.log("Finding latest key for ghost user");
- const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
-
- if (!ghostUserLatestKey) {
- throw new Error("User latest key not found (V2 Upgrade)");
- }
-
- console.log("Creating new project keys for old members");
-
- const newProjectMembers: {
- encryptedKey: string;
- nonce: string;
- senderId: string;
- receiverId: string;
- projectId: string;
- }[] = [];
-
- for (const key of existingProjectKeys) {
- const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
- const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
-
- if (!user || !orgMembership) {
- throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`);
- }
-
- const [newMember] = createWsMembers({
- decryptKey: ghostUserLatestKey,
- userPrivateKey: ghostUser.keys.plainPrivateKey,
- members: [
- {
- userPublicKey: user.publicKey,
- orgMembershipId: orgMembership.id,
- projectMembershipRole: ProjectMembershipRole.Admin
- }
- ]
- });
-
- newProjectMembers.push({
- encryptedKey: newMember.workspaceEncryptedKey,
- nonce: newMember.workspaceEncryptedNonce,
- senderId: ghostUser.user.id,
- receiverId: user.id,
- projectId: project.id
- });
- }
-
- // Create project keys for all the old members
- await projectKeyDAL.insertMany(newProjectMembers, tx);
-
- // Encrypt the bot private key (which is the same as the ghost user)
- const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
-
- // 5. Create a bot for the project
- const newBot = await projectBotDAL.create(
- {
- name: "Infisical Bot (Ghost)",
- projectId: project.id,
- tag,
- iv,
- encryptedPrivateKey: ciphertext,
- isActive: true,
- publicKey: ghostUser.keys.publicKey,
- senderId: ghostUser.user.id,
- encryptedProjectKey: newEncryptedProjectKey,
- encryptedProjectKeyNonce: newEncryptedProjectKeyIv,
- algorithm,
- keyEncoding: encoding
- },
- tx
- );
-
- console.log("Updating secrets with new project key");
- console.log("Got decrypted secrets");
-
- const botPrivateKey = infisicalSymmetricDecrypt({
- keyEncoding: newBot.keyEncoding as SecretKeyEncoding,
- iv: newBot.iv,
- tag: newBot.tag,
- ciphertext: newBot.encryptedPrivateKey
- });
-
- const botKey = decryptAsymmetric({
- ciphertext: newBot.encryptedProjectKey!,
- privateKey: botPrivateKey,
- nonce: newBot.encryptedProjectKeyNonce!,
- publicKey: ghostUser.keys.publicKey
- });
-
- type TPartialSecret = Pick<
- TSecrets,
- | "id"
- | "secretKeyCiphertext"
- | "secretKeyIV"
- | "secretKeyTag"
- | "secretValueCiphertext"
- | "secretValueIV"
- | "secretValueTag"
- | "secretCommentCiphertext"
- | "secretCommentIV"
- | "secretCommentTag"
- >;
-
- const updatedSecrets: TPartialSecret[] = [];
- const updatedSecretVersions: TPartialSecret[] = [];
- const updatedSecretApprovals: TPartialSecret[] = [];
- for (const rawSecret of decryptedSecrets) {
- const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretKey, botKey);
- const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretValue || "", botKey);
- const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(rawSecret.secretComment || "", botKey);
-
- const payload = {
- id: rawSecret.id,
- secretKeyCiphertext: secretKeyEncrypted.ciphertext,
- secretKeyIV: secretKeyEncrypted.iv,
- secretKeyTag: secretKeyEncrypted.tag,
- secretValueCiphertext: secretValueEncrypted.ciphertext,
- secretValueIV: secretValueEncrypted.iv,
- secretValueTag: secretValueEncrypted.tag,
- secretCommentCiphertext: secretCommentEncrypted.ciphertext,
- secretCommentIV: secretCommentEncrypted.iv,
- secretCommentTag: secretCommentEncrypted.tag
- } as const;
-
- if (rawSecret.docType === SecretDocType.Secret) {
- updatedSecrets.push(payload);
- } else if (rawSecret.docType === SecretDocType.SecretVersion) {
- updatedSecretVersions.push(payload);
- } else if (rawSecret.docType === SecretDocType.ApprovalSecret) {
- updatedSecretApprovals.push(payload);
- } else {
- throw new Error("Unknown secret type");
- }
- }
-
- const secretUpdates = await secretDAL.bulkUpdateNoVersionIncrement(
- [
- ...updatedSecrets.map((secret) => ({
- filter: { id: secret.id },
- data: {
- ...secret,
- id: undefined
- }
- }))
- ],
- tx
- );
-
- const secretVersionUpdates = await secretVersionDAL.bulkUpdateNoVersionIncrement(
- [
- ...updatedSecretVersions.map((version) => ({
- filter: { id: version.id },
- data: {
- ...version,
- id: undefined
- }
- }))
- ],
- tx
- );
-
- const secretApprovalUpdates = await secretApprovalSecretDAL.bulkUpdateNoVersionIncrement(
- [
- ...updatedSecretApprovals.map((approval) => ({
- filter: {
- id: approval.id
- },
- data: {
- ...approval,
- id: undefined
- }
- }))
- ],
- tx
- );
-
- if (secretUpdates.length !== updatedSecrets.length) {
- throw new Error("Failed to update some secrets");
- }
- if (secretVersionUpdates.length !== updatedSecretVersions.length) {
- throw new Error("Failed to update some secret versions");
- }
- if (secretApprovalUpdates.length !== updatedSecretApprovals.length) {
- throw new Error("Failed to update some secret approvals");
- }
-
- throw new Error("Transaction was successful");
- });
+ return project.upgradeStatus || null;
};
return {
@@ -700,6 +413,7 @@ export const projectServiceFactory = ({
deleteProject,
getProjects,
findProjectGhostUser,
+ getProjectUpgradeStatus,
getAProject,
toggleAutoCapitalization,
updateName,
diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts
index 9cdc55c8a..faaef42d4 100644
--- a/backend/src/services/super-admin/super-admin-service.ts
+++ b/backend/src/services/super-admin/super-admin-service.ts
@@ -70,6 +70,7 @@ export const superAdminServiceFactory = ({
lastName,
email,
superAdmin: true,
+ ghost: false,
isAccepted: true,
authMethods: [AuthMethod.EMAIL]
},
diff --git a/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx b/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx
new file mode 100644
index 000000000..c0d3b4d59
--- /dev/null
+++ b/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx
@@ -0,0 +1,90 @@
+import { useCallback, useEffect, useState } from "react";
+
+import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
+import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
+import { Workspace } from "@app/hooks/api/types";
+
+import { Alert } from "../Alert";
+import { Button } from "../Button";
+
+export type UpgradeProjectAlertProps = {
+ project: Workspace;
+};
+
+export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.Element | null => {
+ const { createNotification } = useNotificationContext();
+ const upgradeProject = useUpgradeProject();
+ const [currentStatus, setCurrentStatus] = useState(null);
+ const {
+ data: projectStatus,
+ refetch: getLatestProjectStatus,
+ isLoading: statusIsLoading
+ } = useGetUpgradeProjectStatus(project.id);
+
+ 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: project.id,
+ privateKey: PRIVATE_KEY
+ });
+
+ await getLatestProjectStatus();
+ }, []);
+
+ useEffect(() => {
+ if (project.version === "v1") {
+ getLatestProjectStatus();
+ }
+
+ if (projectStatus && projectStatus?.status !== null) {
+ if (projectStatus.status === "IN_PROGRESS") {
+ setCurrentStatus("Your upgrade is being processed.");
+ } else if (projectStatus.status === "FAILED") {
+ setCurrentStatus("Upgrade failed, please try again.");
+ }
+ }
+
+ const interval = setInterval(() => {
+ if (project.version === "v1") {
+ getLatestProjectStatus();
+ }
+ }, 5_000);
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [projectStatus]);
+
+ const isLoading =
+ (upgradeProject.isLoading ||
+ currentStatus !== null ||
+ (currentStatus === null && statusIsLoading)) &&
+ projectStatus?.status !== "FAILED";
+
+ if (project.version !== "v1") return null;
+
+ return (
+
+
+
+ Upgrade your project version to continue receiving the latest improvements and patches.
+ {currentStatus &&
Status: {currentStatus}
}
+
+
+
+ Upgrade
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/v2/UpgradeProjectAlert/index.tsx b/frontend/src/components/v2/UpgradeProjectAlert/index.tsx
new file mode 100644
index 000000000..ab67f86cf
--- /dev/null
+++ b/frontend/src/components/v2/UpgradeProjectAlert/index.tsx
@@ -0,0 +1 @@
+export { UpgradeProjectAlert } from "./UpgradeProjectAlert";
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index 1ad051bc0..e66af93a1 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -7,6 +7,7 @@ export {
useDeleteUserFromWorkspace,
useDeleteWorkspace,
useDeleteWsEnvironment,
+ useGetUpgradeProjectStatus,
useGetUserWorkspaceMemberships,
useGetUserWorkspaces,
useGetWorkspaceAuthorizations,
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index 7ec85c442..443cf45e9 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -24,6 +24,7 @@ export const workspaceKeys = {
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
getWorkspaceIndexStatus: (workspaceId: string) =>
[{ workspaceId }, "workspace-index-status"] as const,
+ getProjectUpgradeStatus: (workspaceId: string) => [{ workspaceId }, "workspace-upgrade-status"],
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
@@ -51,6 +52,14 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
return data;
};
+const fetchProjectUpgradeStatus = async (projectId: string) => {
+ const { data } = await apiRequest.get<{ status: string }>(
+ `/api/v2/workspace/${projectId}/upgrade/status`
+ );
+
+ return data;
+};
+
export const fetchWorkspaceSecrets = async (workspaceId: string) => {
const {
data: { secrets }
@@ -76,6 +85,14 @@ export const useUpgradeProject = () => {
});
};
+export const useGetUpgradeProjectStatus = (projectId: string) => {
+ return useQuery({
+ queryKey: workspaceKeys.getProjectUpgradeStatus(projectId),
+ queryFn: () => fetchProjectUpgradeStatus(projectId),
+ enabled: true
+ });
+};
+
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
return data.workspaces;
diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
index 5fd3898f8..a888b7c34 100644
--- a/frontend/src/hooks/api/workspace/types.ts
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -4,6 +4,7 @@ export type Workspace = {
name: string;
orgId: string;
version: "v1" | "v2";
+ upgradeStatus: string | null;
autoCapitalization: boolean;
environments: WorkspaceEnv[];
slug: string;
diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx
index b7af0e3a4..7ea00831c 100644
--- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx
+++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -14,8 +14,6 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions";
import {
- Alert,
- AlertDescription,
Button,
EmptyState,
IconButton,
@@ -31,6 +29,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
+import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
import { useOrganization, useWorkspace } from "@app/context";
import {
useCreateFolder,
@@ -39,8 +38,7 @@ import {
useGetFoldersByEnv,
useGetProjectSecretsAllEnv,
useGetUserWsKey,
- useUpdateSecretV3,
- useUpgradeProject
+ useUpdateSecretV3
} from "@app/hooks/api";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
@@ -107,7 +105,6 @@ export const SecretOverviewPage = () => {
environments: userAvailableEnvs.map(({ slug }) => slug)
});
- const upgradeProject = useUpgradeProject();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
@@ -201,24 +198,6 @@ 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 handleFolderClick = (path: string) => {
@@ -338,22 +317,7 @@ export const SecretOverviewPage = () => {
- {currentWorkspace?.version === "v1" && (
-
-
-
- Upgrade your project. More filler text More filler text More filler text More filler
- text More filler text More filler text More filler text More filler text More filler
- text More filler text More filler text More filler text{" "}
-
-
-
- Upgrade
-
-
-
-
- )}
+ {currentWorkspace?.version === "v1" && }