mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: completed migration
This commit is contained in:
@@ -1,13 +1,248 @@
|
||||
import { Knex } from "knex";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { ProjectType, TableName } from "../schemas";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
/* eslint-disable no-await-in-loop,no-param-reassign,@typescript-eslint/ban-ts-comment */
|
||||
const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => {
|
||||
const newProjectId = uuidV4();
|
||||
const project = await knex(TableName.Project).where("id", projectId).first();
|
||||
await knex(TableName.Project).insert({
|
||||
...project,
|
||||
type: projectType,
|
||||
// @ts-ignore id is required
|
||||
id: newProjectId,
|
||||
slug: slugify(`${project?.name}-${alphaNumericNanoId(4)}`)
|
||||
});
|
||||
|
||||
const customRoleMapping: Record<string, string> = {};
|
||||
const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId);
|
||||
if (projectCustomRoles.length) {
|
||||
await knex(TableName.ProjectRoles).insert(
|
||||
projectCustomRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
customRoleMapping[el.id] = id;
|
||||
el.id = id;
|
||||
el.projectId = newProjectId;
|
||||
el.permissions = el.permissions ? JSON.stringify(el.permissions) : el.permissions;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
const groupMembershipMapping: Record<string, string> = {};
|
||||
const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId);
|
||||
if (groupMemberships.length) {
|
||||
await knex(TableName.GroupProjectMembership).insert(
|
||||
groupMemberships.map((el) => {
|
||||
const id = uuidV4();
|
||||
groupMembershipMapping[el.id] = id;
|
||||
el.id = id;
|
||||
el.projectId = newProjectId;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn(
|
||||
"projectMembershipId",
|
||||
groupMemberships.map((el) => el.id)
|
||||
);
|
||||
if (groupMembershipRoles.length) {
|
||||
await knex(TableName.GroupProjectMembershipRole).insert(
|
||||
groupMembershipRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
el.id = id;
|
||||
el.projectMembershipId = groupMembershipMapping[el.id];
|
||||
el.customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const identityProjectMembershipMapping: Record<string, string> = {};
|
||||
const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId);
|
||||
if (identities.length) {
|
||||
await knex(TableName.IdentityProjectMembership).insert(
|
||||
identities.map((el) => {
|
||||
const id = uuidV4();
|
||||
identityProjectMembershipMapping[el.id] = id;
|
||||
el.id = id;
|
||||
el.projectId = newProjectId;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn(
|
||||
"projectMembershipId",
|
||||
identities.map((el) => el.id)
|
||||
);
|
||||
if (identitiesRoles.length) {
|
||||
await knex(TableName.IdentityProjectMembershipRole).insert(
|
||||
identitiesRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
el.id = id;
|
||||
el.projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId];
|
||||
el.customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const projectMembershipMapping: Record<string, string> = {};
|
||||
const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId);
|
||||
if (projectUserMembers.length) {
|
||||
await knex(TableName.ProjectMembership).insert(
|
||||
projectUserMembers.map((el) => {
|
||||
const id = uuidV4();
|
||||
projectMembershipMapping[el.id] = id;
|
||||
el.id = id;
|
||||
el.projectId = newProjectId;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn(
|
||||
"projectMembershipId",
|
||||
projectUserMembers.map((el) => el.id)
|
||||
);
|
||||
if (membershipRoles.length) {
|
||||
await knex(TableName.ProjectUserMembershipRole).insert(
|
||||
membershipRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
el.id = id;
|
||||
el.projectMembershipId = projectMembershipMapping[el.projectMembershipId];
|
||||
el.customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true);
|
||||
if (kmsKeys.length) {
|
||||
await knex(TableName.KmsKey).insert(
|
||||
kmsKeys.map((el) => {
|
||||
const id = uuidV4();
|
||||
el.id = id;
|
||||
el.projectId = newProjectId;
|
||||
el.slug = slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first();
|
||||
if (projectBot) {
|
||||
const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId };
|
||||
await knex(TableName.ProjectBot).insert(newProjectBot);
|
||||
}
|
||||
|
||||
const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId);
|
||||
if (projectKeys.length) {
|
||||
await knex(TableName.ProjectKeys).insert(
|
||||
projectKeys.map((el) => {
|
||||
const id = uuidV4();
|
||||
el.id = id;
|
||||
el.projectId = newProjectId;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const serviceTokens = await knex(TableName.ServiceToken).where("projectId", projectId);
|
||||
if (serviceTokens.length) {
|
||||
await knex(TableName.ServiceToken).insert(
|
||||
serviceTokens.map((el) => {
|
||||
el.id = uuidV4();
|
||||
el.projectId = projectId;
|
||||
el.scopes = el.scopes ? JSON.stringify(el.scopes) : el.scopes;
|
||||
return el;
|
||||
})
|
||||
);
|
||||
}
|
||||
return newProjectId;
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
|
||||
if (!hasSplitMappingTable) {
|
||||
await knex.schema.createTable(TableName.ProjectSplitBackfillIds, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("sourceProjectId", 36).notNullable();
|
||||
t.foreign("sourceProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.string("destinationProjectType").notNullable();
|
||||
t.string("destinationProjectId", 36).notNullable();
|
||||
t.foreign("destinationProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
|
||||
if (!hasTypeColumn) {
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.string("type");
|
||||
});
|
||||
|
||||
let projectsToBeTyped;
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
projectsToBeTyped = await knex(TableName.Project).whereNull("type").limit(BATCH_SIZE).select("id");
|
||||
if (projectsToBeTyped.length) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.Project)
|
||||
.whereIn(
|
||||
"id",
|
||||
projectsToBeTyped.map((el) => el.id)
|
||||
)
|
||||
.update({ type: ProjectType.SecretManager });
|
||||
}
|
||||
} while (projectsToBeTyped.length > 0);
|
||||
|
||||
const projectsWithCertificates = await knex(TableName.CertificateAuthority)
|
||||
.distinct("projectId")
|
||||
.select("projectId");
|
||||
/* eslint-disable no-await-in-loop,no-param-reassign */
|
||||
for (const { projectId } of projectsWithCertificates) {
|
||||
const newProjectId = await newProject(knex, projectId, ProjectType.CertificateManager);
|
||||
await knex(TableName.CertificateAuthority).where("projectId", projectId).update({ projectId: newProjectId });
|
||||
await knex(TableName.PkiAlert).where("projectId", projectId).update({ projectId: newProjectId });
|
||||
await knex(TableName.PkiCollection).where("projectId", projectId).update({ projectId: newProjectId });
|
||||
await knex(TableName.ProjectSplitBackfillIds).insert({
|
||||
sourceProjectId: projectId,
|
||||
destinationProjectType: ProjectType.CertificateManager,
|
||||
destinationProjectId: newProjectId
|
||||
});
|
||||
}
|
||||
|
||||
const projectsWithCmek = await knex(TableName.KmsKey)
|
||||
.where("isReserved", false)
|
||||
.whereNotNull("projectId")
|
||||
.distinct("projectId")
|
||||
.select("projectId");
|
||||
for (const { projectId } of projectsWithCmek) {
|
||||
if (projectId) {
|
||||
const newProjectId = await newProject(knex, projectId, ProjectType.Cmek);
|
||||
await knex(TableName.KmsKey)
|
||||
.where({
|
||||
isReserved: false,
|
||||
projectId
|
||||
})
|
||||
.update({ projectId: newProjectId });
|
||||
await knex(TableName.ProjectSplitBackfillIds).insert({
|
||||
sourceProjectId: projectId,
|
||||
destinationProjectType: ProjectType.Cmek,
|
||||
destinationProjectId: newProjectId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.string("type").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +253,9 @@ export async function down(knex: Knex): Promise<void> {
|
||||
t.dropColumn("type");
|
||||
});
|
||||
}
|
||||
|
||||
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
|
||||
if (hasSplitMappingTable) {
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ export enum TableName {
|
||||
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
|
||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||
// junction tables with tags
|
||||
SecretV2JnTag = "secret_v2_tag_junction",
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
|
||||
@@ -428,7 +428,8 @@ export const ORGANIZATIONS = {
|
||||
search: "The text string that identity membership names will be filtered by."
|
||||
},
|
||||
GET_PROJECTS: {
|
||||
organizationId: "The ID of the organization to get projects from."
|
||||
organizationId: "The ID of the organization to get projects from.",
|
||||
type: "The type of project to filter by."
|
||||
},
|
||||
LIST_GROUPS: {
|
||||
organizationId: "The ID of the organization to list groups for."
|
||||
|
||||
@@ -220,6 +220,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
type: true,
|
||||
slug: true,
|
||||
autoCapitalization: true,
|
||||
orgId: true,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ProjectMembershipsSchema,
|
||||
ProjectRolesSchema,
|
||||
ProjectSlackConfigsSchema,
|
||||
ProjectType,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
@@ -135,7 +136,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
includeRoles: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
.transform((value) => value === "true"),
|
||||
type: z.nativeEnum(ProjectType).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -154,7 +156,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
actorOrgId: req.permission.orgId,
|
||||
type: req.query.type
|
||||
});
|
||||
return { workspaces };
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
OrgMembershipsSchema,
|
||||
ProjectMembershipsSchema,
|
||||
ProjectsSchema,
|
||||
ProjectType,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
@@ -76,7 +77,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId)
|
||||
organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId),
|
||||
type: z.nativeEnum(ProjectType).optional().describe(ORGANIZATIONS.GET_PROJECTS.type)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
TProjectUserMembershipRolesInsert,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
@@ -196,26 +195,18 @@ export const orgServiceFactory = ({
|
||||
return org;
|
||||
};
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => {
|
||||
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
|
||||
|
||||
let workspaces: (TProjects & { organization: string } & {
|
||||
environments: {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
}[];
|
||||
})[];
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => {
|
||||
if (actor === ActorType.USER) {
|
||||
workspaces = await projectDAL.findAllProjects(actorId);
|
||||
} else if (actor === ActorType.IDENTITY) {
|
||||
workspaces = await projectDAL.findAllProjectsByIdentity(actorId);
|
||||
} else {
|
||||
throw new BadRequestError({ message: "Invalid actor type" });
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, orgId, type);
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
||||
if (actor === ActorType.IDENTITY) {
|
||||
const workspaces = await projectDAL.findAllProjectsByIdentity(actorId, type);
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid actor type" });
|
||||
};
|
||||
|
||||
const addGhostUser = async (orgId: string, tx?: Knex) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type";
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
|
||||
export type TUpdateOrgMembershipDTO = {
|
||||
userId: string;
|
||||
@@ -55,6 +56,7 @@ export type TFindAllWorkspacesDTO = {
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
orgId: string;
|
||||
type?: ProjectType;
|
||||
};
|
||||
|
||||
export type TUpdateOrgDTO = {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas";
|
||||
import {
|
||||
ProjectsSchema,
|
||||
ProjectType,
|
||||
ProjectUpgradeStatus,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TProjectsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
@@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
||||
export const projectDALFactory = (db: TDbClient) => {
|
||||
const projectOrm = ormify(db, TableName.Project);
|
||||
|
||||
const findAllProjects = async (userId: string) => {
|
||||
const findAllProjects = async (userId: string, orgId: string, projectType?: ProjectType | null) => {
|
||||
try {
|
||||
const workspaces = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
.where({ userId })
|
||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.orgId`, orgId)
|
||||
.andWhere((qb) => {
|
||||
if (projectType) {
|
||||
void qb.where(`${TableName.Project}.type`, projectType);
|
||||
}
|
||||
})
|
||||
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.Project),
|
||||
@@ -31,14 +44,17 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
{ column: `${TableName.Environment}.position`, order: "asc" }
|
||||
]);
|
||||
|
||||
const groups: string[] = await db(TableName.UserGroupMembership)
|
||||
.where({ userId })
|
||||
.select(selectAllTableCols(TableName.UserGroupMembership))
|
||||
.pluck("groupId");
|
||||
const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId");
|
||||
|
||||
const groupWorkspaces = await db(TableName.GroupProjectMembership)
|
||||
.whereIn("groupId", groups)
|
||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.orgId`, orgId)
|
||||
.andWhere((qb) => {
|
||||
if (projectType) {
|
||||
void qb.where(`${TableName.Project}.type`, projectType);
|
||||
}
|
||||
})
|
||||
.whereNotIn(
|
||||
`${TableName.Project}.id`,
|
||||
workspaces.map(({ id }) => id)
|
||||
@@ -108,12 +124,13 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findAllProjectsByIdentity = async (identityId: string) => {
|
||||
const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => {
|
||||
try {
|
||||
const workspaces = await db
|
||||
.replicaNode()(TableName.IdentityProjectMembership)
|
||||
.where({ identityId })
|
||||
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.type`, projectType)
|
||||
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.Project),
|
||||
|
||||
@@ -157,7 +157,6 @@ export const projectServiceFactory = ({
|
||||
type = ProjectType.SecretManager
|
||||
}: TCreateProjectDTO) => {
|
||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||
|
||||
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -432,8 +431,8 @@ export const projectServiceFactory = ({
|
||||
return deletedProject;
|
||||
};
|
||||
|
||||
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId);
|
||||
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId, type }: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
||||
|
||||
if (includeRoles) {
|
||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
||||
|
||||
@@ -85,6 +85,7 @@ export type TDeleteProjectDTO = {
|
||||
|
||||
export type TListProjectsDTO = {
|
||||
includeRoles: boolean;
|
||||
type?: ProjectType | null;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpgradeProjectDTO = {
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
@@ -59,11 +60,12 @@ type TAddProjectFormData = z.infer<typeof formSchema>;
|
||||
interface NewProjectModalProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
projectType: ProjectType;
|
||||
}
|
||||
|
||||
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange">;
|
||||
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange" | "projectType">;
|
||||
|
||||
const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { permission } = useOrgPermission();
|
||||
@@ -124,7 +126,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
projectName: name,
|
||||
projectDescription: description,
|
||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||
template
|
||||
template,
|
||||
type: projectType
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
@@ -145,7 +148,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||
router.push(`/project/${newProjectId}/${projectType}/overview`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create project", type: "error" });
|
||||
@@ -316,14 +319,18 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const NewProjectModal: FC<NewProjectModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
export const NewProjectModal: FC<NewProjectModalProps> = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
projectType
|
||||
}) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Create a new project"
|
||||
subTitle="This project will contain your secrets and configurations."
|
||||
>
|
||||
<NewProjectForm onOpenChange={onOpenChange} />
|
||||
<NewProjectForm onOpenChange={onOpenChange} projectType={projectType} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -41,6 +41,7 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
||||
// handle redirects for project-specific routes
|
||||
useEffect(() => {
|
||||
if (shouldTriggerNoProjectAccess) {
|
||||
console.log(value, workspaceId);
|
||||
createNotification({
|
||||
text: "You are not a member of this project.",
|
||||
type: "info"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const secretsToBeAdded = [
|
||||
{
|
||||
@@ -41,7 +42,8 @@ const initProjectHelper = async ({ projectName }: { projectName: string }) => {
|
||||
const {
|
||||
data: { project }
|
||||
} = await createWorkspace({
|
||||
projectName
|
||||
projectName,
|
||||
type: ProjectType.SecretManager
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { ProjectType } from "../workspace/types";
|
||||
|
||||
export const useImportEnvKey = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -31,7 +32,7 @@ export const useImportEnvKey = () => {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { userKeys } from "../users/query-keys";
|
||||
import { workspaceKeys } from "./query-keys";
|
||||
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
|
||||
import { ProjectType, TUpdateWorkspaceGroupRoleDTO } from "./types";
|
||||
|
||||
export const useAddGroupToWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -83,7 +83,7 @@ export const useLeaveProject = () => {
|
||||
return apiRequest.delete(`/api/v1/workspace/${workspaceId}/leave`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace());
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -95,7 +95,7 @@ export const useMigrateProjectToV3 = () => {
|
||||
return apiRequest.post(`/api/v1/workspace/${workspaceId}/migrate-v3`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
DeleteWorkspaceDTO,
|
||||
NameWorkspaceSecretsDTO,
|
||||
ProjectIdentityOrderBy,
|
||||
ProjectType,
|
||||
TGetUpgradeProjectStatusDTO,
|
||||
TListProjectIdentitiesDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
@@ -82,7 +83,7 @@ export const useUpgradeProject = () => {
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -102,10 +103,11 @@ export const useGetUpgradeProjectStatus = ({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async (includeRoles?: boolean) => {
|
||||
const fetchUserWorkspaces = async (includeRoles?: boolean, type?: ProjectType) => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
|
||||
params: {
|
||||
includeRoles
|
||||
includeRoles,
|
||||
type
|
||||
}
|
||||
});
|
||||
return data.workspaces;
|
||||
@@ -139,8 +141,16 @@ export const useGetWorkspaceById = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserWorkspaces = (includeRoles?: boolean) =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
|
||||
export const useGetUserWorkspaces = ({
|
||||
includeRoles,
|
||||
type
|
||||
}: {
|
||||
includeRoles?: boolean;
|
||||
type?: ProjectType;
|
||||
} = {}) =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace(type || ""), () =>
|
||||
fetchUserWorkspaces(includeRoles, type)
|
||||
);
|
||||
|
||||
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||
@@ -206,33 +216,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
||||
refetchInterval: 4000
|
||||
});
|
||||
|
||||
export const createWorkspace = ({
|
||||
projectName,
|
||||
projectDescription,
|
||||
kmsKeyId,
|
||||
template
|
||||
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
||||
return apiRequest.post("/api/v2/workspace", {
|
||||
projectName,
|
||||
projectDescription,
|
||||
kmsKeyId,
|
||||
template
|
||||
});
|
||||
export const createWorkspace = (
|
||||
dto: CreateWorkspaceDTO
|
||||
): Promise<{ data: { project: Workspace } }> => {
|
||||
return apiRequest.post("/api/v2/workspace", dto);
|
||||
};
|
||||
|
||||
export const useCreateWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
|
||||
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) =>
|
||||
createWorkspace({
|
||||
projectName,
|
||||
projectDescription,
|
||||
kmsKeyId,
|
||||
template
|
||||
template,
|
||||
type
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.data.project.type));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -240,15 +243,19 @@ export const useCreateWorkspace = () => {
|
||||
export const useUpdateProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdateProjectDTO>({
|
||||
mutationFn: ({ projectID, newProjectName, newProjectDescription }) => {
|
||||
return apiRequest.patch(`/api/v1/workspace/${projectID}`, {
|
||||
name: newProjectName,
|
||||
description: newProjectDescription
|
||||
});
|
||||
return useMutation<Workspace, {}, UpdateProjectDTO>({
|
||||
mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => {
|
||||
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
||||
`/api/v1/workspace/${projectID}`,
|
||||
{
|
||||
name: newProjectName,
|
||||
description: newProjectDescription
|
||||
}
|
||||
);
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -256,13 +263,18 @@ export const useUpdateProject = () => {
|
||||
export const useToggleAutoCapitalization = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
|
||||
mutationFn: ({ workspaceID, state }) =>
|
||||
apiRequest.post(`/api/v1/workspace/${workspaceID}/auto-capitalization`, {
|
||||
autoCapitalization: state
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
return useMutation<Workspace, {}, ToggleAutoCapitalizationDTO>({
|
||||
mutationFn: async ({ workspaceID, state }) => {
|
||||
const { data } = await apiRequest.post<{ workspace: Workspace }>(
|
||||
`/api/v1/workspace/${workspaceID}/auto-capitalization`,
|
||||
{
|
||||
autoCapitalization: state
|
||||
}
|
||||
);
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -270,14 +282,15 @@ export const useToggleAutoCapitalization = () => {
|
||||
export const useUpdateWorkspaceVersionLimit = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdatePitVersionLimitDTO>({
|
||||
mutationFn: ({ projectSlug, pitVersionLimit }) => {
|
||||
return apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, {
|
||||
return useMutation<Workspace, {}, UpdatePitVersionLimitDTO>({
|
||||
mutationFn: async ({ projectSlug, pitVersionLimit }) => {
|
||||
const { data } = await apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, {
|
||||
pitVersionLimit
|
||||
});
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -285,14 +298,18 @@ export const useUpdateWorkspaceVersionLimit = () => {
|
||||
export const useUpdateWorkspaceAuditLogsRetention = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdateAuditLogsRetentionDTO>({
|
||||
mutationFn: ({ projectSlug, auditLogsRetentionDays }) => {
|
||||
return apiRequest.put(`/api/v1/workspace/${projectSlug}/audit-logs-retention`, {
|
||||
auditLogsRetentionDays
|
||||
});
|
||||
return useMutation<Workspace, {}, UpdateAuditLogsRetentionDTO>({
|
||||
mutationFn: async ({ projectSlug, auditLogsRetentionDays }) => {
|
||||
const { data } = await apiRequest.put(
|
||||
`/api/v1/workspace/${projectSlug}/audit-logs-retention`,
|
||||
{
|
||||
auditLogsRetentionDays
|
||||
}
|
||||
);
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -300,12 +317,13 @@ export const useUpdateWorkspaceAuditLogsRetention = () => {
|
||||
export const useDeleteWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, DeleteWorkspaceDTO>({
|
||||
mutationFn: ({ workspaceID }) => {
|
||||
return apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
|
||||
return useMutation<Workspace, {}, DeleteWorkspaceDTO>({
|
||||
mutationFn: async ({ workspaceID }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
|
||||
return data.workspace;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
onSuccess: (dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
|
||||
queryClient.invalidateQueries(["org-admin-projects"]);
|
||||
}
|
||||
});
|
||||
@@ -322,7 +340,7 @@ export const useCreateWsEnvironment = () => {
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -339,7 +357,7 @@ export const useUpdateWsEnvironment = () => {
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -352,7 +370,7 @@ export const useDeleteWsEnvironment = () => {
|
||||
return apiRequest.delete(`/api/v1/workspace/${workspaceId}/environments/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,7 +11,8 @@ export const workspaceKeys = {
|
||||
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
|
||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||
getAllUserWorkspace: ["workspaces"] as const,
|
||||
getAllUserWorkspace: (type?: string) =>
|
||||
type ? ["workspaces", { type }] : (["workspace"] as const),
|
||||
getWorkspaceAuditLogs: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-audit-logs"] as const,
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
||||
|
||||
@@ -8,6 +8,12 @@ export enum ProjectVersion {
|
||||
V3 = 3
|
||||
}
|
||||
|
||||
export enum ProjectType {
|
||||
SecretManager = "secret-manager",
|
||||
CertificateManager = "cert-manager",
|
||||
Cmek = "cmek"
|
||||
}
|
||||
|
||||
export enum ProjectUserMembershipTemporaryMode {
|
||||
Relative = "relative"
|
||||
}
|
||||
@@ -16,6 +22,7 @@ export type Workspace = {
|
||||
__v: number;
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProjectType;
|
||||
description?: string;
|
||||
orgId: string;
|
||||
version: ProjectVersion;
|
||||
@@ -59,6 +66,7 @@ export type CreateWorkspaceDTO = {
|
||||
projectDescription?: string;
|
||||
kmsKeyId?: string;
|
||||
template?: string;
|
||||
type: ProjectType;
|
||||
};
|
||||
|
||||
export type UpdateProjectDTO = {
|
||||
|
||||
@@ -163,7 +163,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
!router.asPath.includes("secret-scanning") &&
|
||||
!router.asPath.includes("integration")))
|
||||
) {
|
||||
router.push(`/org/${currentOrg?.id}/overview`);
|
||||
router.push(`/org/${currentOrg?.id}/secret-manager/overview`);
|
||||
}
|
||||
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
||||
|
||||
@@ -216,7 +216,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<div className="flex h-12 cursor-default items-center px-3 pt-6">
|
||||
{(router.asPath.includes("project") ||
|
||||
router.asPath.includes("integrations")) && (
|
||||
<Link href={`/org/${currentOrg?.id}/overview`}>
|
||||
<Link href={`/org/${currentOrg?.id}/${currentWorkspace?.type}/overview`}>
|
||||
<div className="pl-1 pr-2 text-mineshaft-400 duration-200 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</div>
|
||||
@@ -379,7 +379,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
(!router.asPath.includes("personal") && currentWorkspace ? (
|
||||
<ProjectSelect />
|
||||
) : (
|
||||
<Link href={`/org/${currentOrg?.id}/overview`}>
|
||||
<Link href={`/org/${currentOrg?.id}/${currentWorkspace?.type}/overview`}>
|
||||
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
|
||||
Back to organization
|
||||
@@ -493,13 +493,33 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</Menu>
|
||||
) : (
|
||||
<Menu className="mt-4">
|
||||
<Link href={`/org/${currentOrg?.id}/overview`} passHref>
|
||||
<Link href={`/org/${currentOrg?.id}/secret-manager/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/overview")}
|
||||
isSelected={router.asPath.includes("/secret-manager/overview")}
|
||||
icon="system-outline-165-view-carousel"
|
||||
>
|
||||
Overview
|
||||
Secret Manager
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?.id}/cert-manager/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/cert-manager/overview")}
|
||||
icon="system-outline-165-view-carousel"
|
||||
>
|
||||
Cert Manager
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?.id}/cmek/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/cmek/overview")}
|
||||
icon="system-outline-165-view-carousel"
|
||||
>
|
||||
Cmek
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faBook,
|
||||
faCoins,
|
||||
faEnvelope,
|
||||
faGear,
|
||||
faPlus,
|
||||
faRightFromBracket
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { TFunction } from "i18next";
|
||||
|
||||
import guidGenerator from "@app/components/utilities/randomId";
|
||||
import { useOrganization, useSubscription, useUser } from "@app/context";
|
||||
import { useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
|
||||
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faSlack} />,
|
||||
t("nav.support.slack"),
|
||||
"https://infisical.com/slack"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faBook} />,
|
||||
t("nav.support.docs"),
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faGithub} />,
|
||||
t("nav.support.issue"),
|
||||
"https://github.com/Infisical/infisical-cli/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faEnvelope} />,
|
||||
t("nav.support.email"),
|
||||
"mailto:support@infisical.com"
|
||||
]
|
||||
];
|
||||
|
||||
export interface ICurrentOrg {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the navigation bar in the main app.
|
||||
* It has two main components: support options and user menu (inlcudes billing, logout, org/user settings)
|
||||
* @returns NavBar
|
||||
*/
|
||||
export const Navbar = () => {
|
||||
const router = useRouter();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { currentOrg, orgs } = useOrganization();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
const { user } = useUser();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// remove this memo
|
||||
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
|
||||
|
||||
const closeApp = async () => {
|
||||
try {
|
||||
console.log("Logging out...");
|
||||
await logout.mutateAsync();
|
||||
localStorage.removeItem("protectedKey");
|
||||
localStorage.removeItem("protectedKeyIV");
|
||||
localStorage.removeItem("protectedKeyTag");
|
||||
localStorage.removeItem("publicKey");
|
||||
localStorage.removeItem("encryptedPrivateKey");
|
||||
localStorage.removeItem("iv");
|
||||
localStorage.removeItem("tag");
|
||||
localStorage.removeItem("PRIVATE_KEY");
|
||||
localStorage.removeItem("orgData.id");
|
||||
localStorage.removeItem("projectData.id");
|
||||
router.push("/login");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-[70] border-b border-mineshaft-500 bg-mineshaft-900 text-white">
|
||||
<div className="flex w-full justify-between px-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
|
||||
</div>
|
||||
<a href="#" className="mx-2 text-2xl font-semibold text-white">
|
||||
Infisical
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative z-40 mx-2 flex items-center justify-start">
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mr-4 flex items-center rounded-md px-3 py-2 text-sm text-gray-200 duration-200 hover:bg-white/10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBook} className="mr-2 text-xl" />
|
||||
Docs
|
||||
</a>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div className="mr-4">
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-20 mt-0.5 w-64 origin-top-right rounded-md border border-mineshaft-700 bg-bunker px-2 py-1.5 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{supportOptionsList.map(([icon, text, url]) => (
|
||||
<a
|
||||
key={guidGenerator()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md py-0.5 font-normal text-gray-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/10 hover:text-gray-200">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<Menu as="div" className="relative mr-4 inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md py-2 pr-2 pl-2 text-sm font-medium text-gray-200 duration-200 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
{user?.firstName} {user?.lastName}
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleDown}
|
||||
className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="w-68 absolute right-0 z-[125] mt-0.5 origin-top-right divide-y divide-mineshaft-700 rounded-md border border-mineshaft-700 bg-mineshaft-900 shadow-lg ring-1 ring-black ring-opacity-5 drop-shadow-2xl focus:outline-none">
|
||||
<div className="px-1 py-1">
|
||||
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.user.signed-in-as")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push("/personal-settings")}
|
||||
className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5"
|
||||
>
|
||||
<div className="flex h-8 w-9 items-center justify-center rounded-full bg-white/10 text-gray-300">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<p className="px-2 pt-1 text-sm text-gray-300">
|
||||
{" "}
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="px-2 pb-1 text-xs text-gray-400">{user?.email}</p>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="mr-1 cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.user.current-organization")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
className="mt-2 flex cursor-pointer flex-row items-center rounded-md px-2 py-1 hover:bg-white/5"
|
||||
>
|
||||
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="px-2 text-sm text-gray-300">{currentOrg?.name}</p>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="cursor-pointer rounded-md p-2 text-lg text-gray-400 hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{subscription && subscription.slug !== null && (
|
||||
<button
|
||||
// onClick={buttonAction}
|
||||
type="button"
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
|
||||
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/5 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon className="pl-1.5 pr-3 text-lg" icon={faCoins} />
|
||||
<div className="text-sm">{t("nav.user.usage-billing")}</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
// onClick={buttonAction}
|
||||
className="mb-2 w-full cursor-pointer"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
|
||||
className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-primary/100 hover:font-semibold hover:text-black"
|
||||
>
|
||||
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
||||
</span>
|
||||
<div className="ml-1 text-sm">{t("nav.user.invite")}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{orgs && orgs?.length > 1 && (
|
||||
<div className="px-1 pt-1">
|
||||
<div className="ml-2 mt-2 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.user.other-organizations")}
|
||||
</div>
|
||||
<div className="mt-3 mb-2 flex flex-col items-start px-1">
|
||||
{orgs
|
||||
?.filter((org: { id: string }) => org.id !== currentOrg?.id)
|
||||
.map((org: { id: string; name: string }) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={guidGenerator()}
|
||||
onClick={() => {
|
||||
localStorage.setItem("orgData.id", org.id);
|
||||
router.reload();
|
||||
}}
|
||||
className="flex w-full cursor-pointer flex-row items-center justify-start rounded-md p-1.5 hover:bg-white/5"
|
||||
>
|
||||
<div className="flex h-7 w-8 items-center justify-center rounded-md bg-white/10 text-gray-300">
|
||||
{org.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="px-2 text-sm text-gray-300">{org.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeApp}
|
||||
className={`${
|
||||
active ? "bg-red font-semibold text-white" : "text-gray-400"
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
>
|
||||
<div className="relative flex cursor-pointer select-none items-center justify-start">
|
||||
<FontAwesomeIcon
|
||||
className="ml-1.5 mr-3 text-lg"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
{t("common.logout")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
<div className="mx-auto w-full border-t border-mineshaft-500 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="mx-auto py-4 text-center text-sm"
|
||||
>
|
||||
You are currently on the <span className="font-semibold">Starter</span> plan. Unlock the
|
||||
full power of Infisical on the{" "}
|
||||
<span className="font-semibold">Pro Free Trial →</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean };
|
||||
|
||||
@@ -138,6 +138,7 @@ export const ProjectSelect = () => {
|
||||
|
||||
const { options, value } = useMemo(() => {
|
||||
const projectOptions = workspaces
|
||||
.filter((el) => el.type === currentWorkspace?.type)
|
||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||
...w,
|
||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||
@@ -206,6 +207,7 @@ export const ProjectSelect = () => {
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
projectType={currentWorkspace?.type || ProjectType.SecretManager}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
8
frontend/src/pages/org/[id]/cert-manager/overview.tsx
Normal file
8
frontend/src/pages/org/[id]/cert-manager/overview.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { ProductOverview } from "../secret-manager/overview";
|
||||
|
||||
const CertManagerOverviewPage = () => <ProductOverview type={ProjectType.CertificateManager} />;
|
||||
|
||||
Object.assign(CertManagerOverviewPage, { requireAuth: true });
|
||||
|
||||
export default CertManagerOverviewPage;
|
||||
8
frontend/src/pages/org/[id]/cmek/overview.tsx
Normal file
8
frontend/src/pages/org/[id]/cmek/overview.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { ProductOverview } from "../secret-manager/overview";
|
||||
|
||||
const CmekManagerOverviewPage = () => <ProductOverview type={ProjectType.Cmek} />;
|
||||
|
||||
Object.assign(CmekManagerOverviewPage, { requireAuth: true });
|
||||
|
||||
export default CmekManagerOverviewPage;
|
||||
File diff suppressed because it is too large
Load Diff
1059
frontend/src/pages/org/[id]/secret-manager/overview.tsx
Normal file
1059
frontend/src/pages/org/[id]/secret-manager/overview.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,9 @@ export const AddOrgMemberModal = ({
|
||||
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
|
||||
const { data: projects, isLoading: isProjectsLoading } = useGetUserWorkspaces(true);
|
||||
const { data: projects, isLoading: isProjectsLoading } = useGetUserWorkspaces({
|
||||
includeRoles: true
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useProjectPermission, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetWorkspaceById, useMigrateProjectToV3, workspaceKeys } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
enum ProjectUpgradeStatus {
|
||||
InProgress = "IN_PROGRESS",
|
||||
@@ -53,7 +53,7 @@ export const SecretV2MigrationSection = () => {
|
||||
if (isProjectUpgraded && migrateProjectToV3.data) {
|
||||
createNotification({ type: "success", text: "Project upgrade completed successfully" });
|
||||
migrateProjectToV3.reset();
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager));
|
||||
}
|
||||
}, [isProjectUpgraded, Boolean(migrateProjectToV3.data)]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user