mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
27 Commits
misc/addre
...
max/connec
Author | SHA1 | Date | |
---|---|---|---|
|
5ecb660cdd | ||
|
64a982d5e0 | ||
|
1080438ad8 | ||
|
eb3acae332 | ||
|
a0b3520899 | ||
|
2f6f359ddf | ||
|
df8c1e54e0 | ||
|
cac060deff | ||
|
47269bc95b | ||
|
8502e9a1d8 | ||
|
d89eb4fa84 | ||
|
ca7ab4eaf1 | ||
|
c57fc5e3f1 | ||
|
9b4e1f561e | ||
|
097fcad5ae | ||
|
d1547564f9 | ||
|
24acb98978 | ||
|
0fd8274ff0 | ||
|
a857375cc1 | ||
|
69bf9dc20f | ||
|
5151c91760 | ||
|
f12d8b6f89 | ||
|
695c499448 | ||
|
dc715cc238 | ||
|
639057415f | ||
|
c38dae2319 | ||
|
39f71f9488 |
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -202,6 +202,9 @@ import {
|
||||
TProjectSlackConfigs,
|
||||
TProjectSlackConfigsInsert,
|
||||
TProjectSlackConfigsUpdate,
|
||||
TProjectSplitBackfillIds,
|
||||
TProjectSplitBackfillIdsInsert,
|
||||
TProjectSplitBackfillIdsUpdate,
|
||||
TProjectsUpdate,
|
||||
TProjectTemplates,
|
||||
TProjectTemplatesInsert,
|
||||
@@ -838,5 +841,10 @@ declare module "knex/types/tables" {
|
||||
TProjectTemplatesUpdate
|
||||
>;
|
||||
[TableName.TotpConfig]: KnexOriginal.CompositeTableType<TTotpConfigs, TTotpConfigsInsert, TTotpConfigsUpdate>;
|
||||
[TableName.ProjectSplitBackfillIds]: KnexOriginal.CompositeTableType<
|
||||
TProjectSplitBackfillIds,
|
||||
TProjectSplitBackfillIdsInsert,
|
||||
TProjectSplitBackfillIdsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,297 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { ProjectType, TableName } from "../schemas";
|
||||
|
||||
/* eslint-disable no-await-in-loop,@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.batchInsert(
|
||||
TableName.ProjectRoles,
|
||||
projectCustomRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
customRoleMapping[el.id] = id;
|
||||
return {
|
||||
...el,
|
||||
id,
|
||||
projectId: newProjectId,
|
||||
permissions: el.permissions ? JSON.stringify(el.permissions) : el.permissions
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
const groupMembershipMapping: Record<string, string> = {};
|
||||
const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId);
|
||||
if (groupMemberships.length) {
|
||||
await knex.batchInsert(
|
||||
TableName.GroupProjectMembership,
|
||||
groupMemberships.map((el) => {
|
||||
const id = uuidV4();
|
||||
groupMembershipMapping[el.id] = id;
|
||||
return { ...el, id, projectId: newProjectId };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn(
|
||||
"projectMembershipId",
|
||||
groupMemberships.map((el) => el.id)
|
||||
);
|
||||
if (groupMembershipRoles.length) {
|
||||
await knex.batchInsert(
|
||||
TableName.GroupProjectMembershipRole,
|
||||
groupMembershipRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
const projectMembershipId = groupMembershipMapping[el.projectMembershipId];
|
||||
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
|
||||
return { ...el, id, projectMembershipId, customRoleId };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const identityProjectMembershipMapping: Record<string, string> = {};
|
||||
const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId);
|
||||
if (identities.length) {
|
||||
await knex.batchInsert(
|
||||
TableName.IdentityProjectMembership,
|
||||
identities.map((el) => {
|
||||
const id = uuidV4();
|
||||
identityProjectMembershipMapping[el.id] = id;
|
||||
return { ...el, id, projectId: newProjectId };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn(
|
||||
"projectMembershipId",
|
||||
identities.map((el) => el.id)
|
||||
);
|
||||
if (identitiesRoles.length) {
|
||||
await knex.batchInsert(
|
||||
TableName.IdentityProjectMembershipRole,
|
||||
identitiesRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
const projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId];
|
||||
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
|
||||
return { ...el, id, projectMembershipId, customRoleId };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const projectMembershipMapping: Record<string, string> = {};
|
||||
const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId);
|
||||
if (projectUserMembers.length) {
|
||||
await knex.batchInsert(
|
||||
TableName.ProjectMembership,
|
||||
projectUserMembers.map((el) => {
|
||||
const id = uuidV4();
|
||||
projectMembershipMapping[el.id] = id;
|
||||
return { ...el, id, projectId: newProjectId };
|
||||
})
|
||||
);
|
||||
}
|
||||
const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn(
|
||||
"projectMembershipId",
|
||||
projectUserMembers.map((el) => el.id)
|
||||
);
|
||||
if (membershipRoles.length) {
|
||||
await knex.batchInsert(
|
||||
TableName.ProjectUserMembershipRole,
|
||||
membershipRoles.map((el) => {
|
||||
const id = uuidV4();
|
||||
const projectMembershipId = projectMembershipMapping[el.projectMembershipId];
|
||||
const customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId;
|
||||
return { ...el, id, projectMembershipId, customRoleId };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true);
|
||||
if (kmsKeys.length) {
|
||||
await knex.batchInsert(
|
||||
TableName.KmsKey,
|
||||
kmsKeys.map((el) => {
|
||||
const id = uuidV4();
|
||||
const slug = slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
return { ...el, id, slug, projectId: newProjectId };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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.batchInsert(
|
||||
TableName.ProjectKeys,
|
||||
projectKeys.map((el) => {
|
||||
const id = uuidV4();
|
||||
return { ...el, id, projectId: newProjectId };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return newProjectId;
|
||||
};
|
||||
|
||||
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.KMS);
|
||||
await knex(TableName.KmsKey)
|
||||
.where({
|
||||
isReserved: false,
|
||||
projectId
|
||||
})
|
||||
.update({ projectId: newProjectId });
|
||||
await knex(TableName.ProjectSplitBackfillIds).insert({
|
||||
sourceProjectId: projectId,
|
||||
destinationProjectType: ProjectType.KMS,
|
||||
destinationProjectId: newProjectId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable */
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.string("type").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type");
|
||||
const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds);
|
||||
|
||||
if (hasTypeColumn && hasSplitMappingTable) {
|
||||
const splitProjectMappings = await knex(TableName.ProjectSplitBackfillIds).where({});
|
||||
const certMapping = splitProjectMappings.filter(
|
||||
(el) => el.destinationProjectType === ProjectType.CertificateManager
|
||||
);
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const project of certMapping) {
|
||||
await knex(TableName.CertificateAuthority)
|
||||
.where("projectId", project.destinationProjectId)
|
||||
.update({ projectId: project.sourceProjectId });
|
||||
await knex(TableName.PkiAlert)
|
||||
.where("projectId", project.destinationProjectId)
|
||||
.update({ projectId: project.sourceProjectId });
|
||||
await knex(TableName.PkiCollection)
|
||||
.where("projectId", project.destinationProjectId)
|
||||
.update({ projectId: project.sourceProjectId });
|
||||
}
|
||||
|
||||
/* eslint-enable */
|
||||
const kmsMapping = splitProjectMappings.filter((el) => el.destinationProjectType === ProjectType.KMS);
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const project of kmsMapping) {
|
||||
await knex(TableName.KmsKey)
|
||||
.where({
|
||||
isReserved: false,
|
||||
projectId: project.destinationProjectId
|
||||
})
|
||||
.update({ projectId: project.sourceProjectId });
|
||||
}
|
||||
/* eslint-enable */
|
||||
await knex(TableName.ProjectMembership)
|
||||
.whereIn(
|
||||
"projectId",
|
||||
splitProjectMappings.map((el) => el.destinationProjectId)
|
||||
)
|
||||
.delete();
|
||||
await knex(TableName.ProjectRoles)
|
||||
.whereIn(
|
||||
"projectId",
|
||||
splitProjectMappings.map((el) => el.destinationProjectId)
|
||||
)
|
||||
.delete();
|
||||
await knex(TableName.Project)
|
||||
.whereIn(
|
||||
"id",
|
||||
splitProjectMappings.map((el) => el.destinationProjectId)
|
||||
)
|
||||
.delete();
|
||||
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.dropColumn("type");
|
||||
});
|
||||
}
|
||||
|
||||
if (hasSplitMappingTable) {
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds);
|
||||
}
|
||||
}
|
@@ -65,6 +65,7 @@ export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
export * from "./project-slack-configs";
|
||||
export * from "./project-split-backfill-ids";
|
||||
export * from "./project-templates";
|
||||
export * from "./project-user-additional-privilege";
|
||||
export * from "./project-user-membership-roles";
|
||||
|
@@ -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",
|
||||
@@ -200,3 +201,9 @@ export enum IdentityAuthMethod {
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
JWT_AUTH = "jwt-auth"
|
||||
}
|
||||
|
||||
export enum ProjectType {
|
||||
SecretManager = "secret-manager",
|
||||
CertificateManager = "cert-manager",
|
||||
KMS = "kms"
|
||||
}
|
||||
|
21
backend/src/db/schemas/project-split-backfill-ids.ts
Normal file
21
backend/src/db/schemas/project-split-backfill-ids.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectSplitBackfillIdsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
sourceProjectId: z.string(),
|
||||
destinationProjectType: z.string(),
|
||||
destinationProjectId: z.string()
|
||||
});
|
||||
|
||||
export type TProjectSplitBackfillIds = z.infer<typeof ProjectSplitBackfillIdsSchema>;
|
||||
export type TProjectSplitBackfillIdsInsert = Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>;
|
||||
export type TProjectSplitBackfillIdsUpdate = Partial<
|
||||
Omit<z.input<typeof ProjectSplitBackfillIdsSchema>, TImmutableDBKeys>
|
||||
>;
|
@@ -24,7 +24,8 @@ export const ProjectsSchema = z.object({
|
||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
|
||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||
description: z.string().nullable().optional()
|
||||
description: z.string().nullable().optional(),
|
||||
type: z.string()
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
@@ -4,7 +4,7 @@ import { Knex } from "knex";
|
||||
|
||||
import { encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
|
||||
import { ProjectMembershipRole, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas";
|
||||
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data";
|
||||
|
||||
export const DEFAULT_PROJECT_ENVS = [
|
||||
@@ -24,6 +24,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
name: seedData1.project.name,
|
||||
orgId: seedData1.organization.id,
|
||||
slug: "first-project",
|
||||
type: ProjectType.SecretManager,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.project.id
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas";
|
||||
import { ProjectMembershipRole, ProjectType, ProjectVersion, TableName } from "../schemas";
|
||||
import { seedData1 } from "../seed-data";
|
||||
|
||||
export const DEFAULT_PROJECT_ENVS = [
|
||||
@@ -16,6 +16,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
orgId: seedData1.organization.id,
|
||||
slug: seedData1.projectV3.slug,
|
||||
version: ProjectVersion.V3,
|
||||
type: ProjectType.SecretManager,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.projectV3.id
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -86,13 +87,15 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
if (!groupApprovers && approvals > userApprovers.length + userApproverNames.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
@@ -190,14 +193,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
// Anyone in the project should be able to get the policies.
|
||||
/* const { permission } = */ await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId);
|
||||
|
||||
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
|
||||
return accessApprovalPolicies;
|
||||
@@ -241,13 +237,14 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
if (!accessApprovalPolicy) {
|
||||
throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
accessApprovalPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
@@ -324,13 +321,14 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
const policy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!policy) throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
policy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
@@ -67,13 +67,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
@@ -146,13 +147,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
@@ -225,13 +227,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { ProjectType, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
@@ -73,13 +73,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
|
||||
const projectId = project.id;
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
@@ -144,13 +145,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
|
||||
const projectId = project.id;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
@@ -227,13 +229,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
|
||||
const projectId = project.id;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
||||
|
@@ -269,6 +269,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
);
|
||||
|
||||
@@ -284,13 +285,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
membershipCreatedAt,
|
||||
groupMembershipCreatedAt,
|
||||
groupMembershipUpdatedAt,
|
||||
membershipUpdatedAt
|
||||
membershipUpdatedAt,
|
||||
projectType
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
projectId,
|
||||
username,
|
||||
projectType,
|
||||
id: membershipId || groupMembershipId,
|
||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||
@@ -449,6 +452,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
@@ -480,7 +484,14 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
const permission = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "membershipId",
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
|
||||
parentMapper: ({
|
||||
membershipId,
|
||||
membershipCreatedAt,
|
||||
membershipUpdatedAt,
|
||||
orgId,
|
||||
identityName,
|
||||
projectType
|
||||
}) => ({
|
||||
id: membershipId,
|
||||
identityId,
|
||||
username: identityName,
|
||||
@@ -488,6 +499,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
createdAt: membershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt,
|
||||
orgId,
|
||||
projectType,
|
||||
// just a prefilled value
|
||||
orgAuthEnforced: false
|
||||
}),
|
||||
|
@@ -6,6 +6,7 @@ import handlebars from "handlebars";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
ProjectMembershipRole,
|
||||
ProjectType,
|
||||
ServiceTokenScopes,
|
||||
TIdentityProjectMemberships,
|
||||
TProjectMemberships
|
||||
@@ -255,6 +256,13 @@ export const permissionServiceFactory = ({
|
||||
return {
|
||||
permission,
|
||||
membership: userProjectPermission,
|
||||
ForbidOnInvalidProjectType: (productType: ProjectType) => {
|
||||
if (productType !== userProjectPermission.projectType) {
|
||||
throw new BadRequestError({
|
||||
message: `The project is of type ${userProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
|
||||
});
|
||||
}
|
||||
},
|
||||
hasRole: (role: string) =>
|
||||
userProjectPermission.roles.findIndex(
|
||||
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
|
||||
@@ -323,6 +331,13 @@ export const permissionServiceFactory = ({
|
||||
return {
|
||||
permission,
|
||||
membership: identityProjectPermission,
|
||||
ForbidOnInvalidProjectType: (productType: ProjectType) => {
|
||||
if (productType !== identityProjectPermission.projectType) {
|
||||
throw new BadRequestError({
|
||||
message: `The project is of type ${identityProjectPermission.projectType}. Operations of type ${productType} are not allowed.`
|
||||
});
|
||||
}
|
||||
},
|
||||
hasRole: (role: string) =>
|
||||
identityProjectPermission.roles.findIndex(
|
||||
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
|
||||
@@ -361,7 +376,14 @@ export const permissionServiceFactory = ({
|
||||
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
|
||||
return {
|
||||
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
|
||||
membership: undefined
|
||||
membership: undefined,
|
||||
ForbidOnInvalidProjectType: (productType: ProjectType) => {
|
||||
if (productType !== serviceTokenProject.type) {
|
||||
throw new BadRequestError({
|
||||
message: `The project is of type ${serviceTokenProject.type}. Operations of type ${productType} are not allowed.`
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -370,6 +392,7 @@ export const permissionServiceFactory = ({
|
||||
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
membership: undefined;
|
||||
hasRole: (arg: string) => boolean;
|
||||
ForbidOnInvalidProjectType: (type: ProjectType) => void;
|
||||
} // service token doesn't have both membership and roles
|
||||
: {
|
||||
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
@@ -379,6 +402,7 @@ export const permissionServiceFactory = ({
|
||||
roles: Array<{ role: string }>;
|
||||
};
|
||||
hasRole: (role: string) => boolean;
|
||||
ForbidOnInvalidProjectType: (type: ProjectType) => void;
|
||||
};
|
||||
|
||||
const getProjectPermission = async <T extends ActorType>(
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import picomatch from "picomatch";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -78,13 +79,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
if (!groupApprovers.length && approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
@@ -191,13 +193,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@@ -285,13 +288,14 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
if (!sapPolicy)
|
||||
throw new NotFoundError({ message: `Secret approval policy with ID '${secretPolicyId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
sapPolicy.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
|
@@ -2,6 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
ProjectType,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
SecretType,
|
||||
@@ -875,13 +876,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}: TGenerateSecretApprovalRequestDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
@@ -1155,14 +1157,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (actor === ActorType.SERVICE || actor === ActorType.Machine)
|
||||
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new NotFoundError({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import Ajv from "ajv";
|
||||
|
||||
import { ProjectVersion, TableName } from "@app/db/schemas";
|
||||
import { ProjectType, ProjectVersion, TableName } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
@@ -53,13 +53,14 @@ export const secretRotationServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
|
||||
return {
|
||||
@@ -81,13 +82,14 @@ export const secretRotationServiceFactory = ({
|
||||
secretPath,
|
||||
environment
|
||||
}: TCreateSecretRotationDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
@@ -234,13 +236,14 @@ export const secretRotationServiceFactory = ({
|
||||
message: "Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
doc.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
|
||||
await secretRotationQueue.removeFromQueue(doc.id, doc.interval);
|
||||
await secretRotationQueue.addToQueue(doc.id, doc.interval);
|
||||
@@ -251,13 +254,14 @@ export const secretRotationServiceFactory = ({
|
||||
const doc = await secretRotationDAL.findById(rotationId);
|
||||
if (!doc) throw new NotFoundError({ message: `Rotation with ID '${rotationId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
doc.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
||||
import { ProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
@@ -322,13 +322,14 @@ export const secretSnapshotServiceFactory = ({
|
||||
if (!snapshot) throw new NotFoundError({ message: `Snapshot with ID '${snapshotId}' not found` });
|
||||
const shouldUseBridge = snapshot.projectVersion === 3;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
snapshot.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
|
@@ -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."
|
||||
|
@@ -757,7 +757,8 @@ export const registerRoutes = async (
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
smtpService,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const pkiCollectionService = pkiCollectionServiceFactory({
|
||||
@@ -765,7 +766,8 @@ export const registerRoutes = async (
|
||||
pkiCollectionItemDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const projectTemplateService = projectTemplateServiceFactory({
|
||||
@@ -1273,7 +1275,8 @@ export const registerRoutes = async (
|
||||
const cmekService = cmekServiceFactory({
|
||||
kmsDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
permissionService,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const externalMigrationQueue = externalMigrationQueueFactory({
|
||||
|
@@ -220,6 +220,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
type: true,
|
||||
slug: true,
|
||||
autoCapitalization: true,
|
||||
orgId: true,
|
||||
|
@@ -328,7 +328,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true, type: 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.enum([ProjectType.SecretManager, ProjectType.KMS, ProjectType.CertificateManager, "all"]).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";
|
||||
@@ -78,6 +79,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
type: z.nativeEnum(ProjectType).optional().describe(ORGANIZATIONS.GET_PROJECTS.type)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaces: z
|
||||
@@ -104,7 +108,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId
|
||||
orgId: req.params.organizationId,
|
||||
type: req.query.type
|
||||
});
|
||||
|
||||
return { workspaces };
|
||||
@@ -281,7 +286,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
lastName: true,
|
||||
id: true
|
||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
||||
project: ProjectsSchema.pick({ name: true, id: true }),
|
||||
project: ProjectsSchema.pick({ name: true, id: true, type: true }),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
|
@@ -5,7 +5,8 @@ import {
|
||||
CertificatesSchema,
|
||||
PkiAlertsSchema,
|
||||
PkiCollectionsSchema,
|
||||
ProjectKeysSchema
|
||||
ProjectKeysSchema,
|
||||
ProjectType
|
||||
} from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||
@@ -159,7 +160,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
template: slugSchema({ field: "Template Name", max: 64 })
|
||||
.optional()
|
||||
.default(InfisicalProjectTemplate.Default)
|
||||
.describe(PROJECTS.CREATE.template)
|
||||
.describe(PROJECTS.CREATE.template),
|
||||
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -178,7 +180,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
workspaceDescription: req.body.projectDescription,
|
||||
slug: req.body.slug,
|
||||
kmsKeyId: req.body.kmsKeyId,
|
||||
template: req.body.template
|
||||
template: req.body.template,
|
||||
type: req.body.type
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
|
@@ -5,7 +5,7 @@ import crypto, { KeyObject } from "crypto";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -77,7 +77,10 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
projectDAL: Pick<
|
||||
TProjectDALFactory,
|
||||
"findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@@ -123,14 +126,24 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}: TCreateCaDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
let projectId = project.id;
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -161,7 +174,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
const ca = await certificateAuthorityDAL.create(
|
||||
{
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
type,
|
||||
organization,
|
||||
ou,
|
||||
@@ -185,7 +198,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
@@ -323,13 +336,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -348,13 +362,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
@@ -434,13 +449,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -819,13 +835,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: "CA not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -965,13 +982,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -1127,13 +1145,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new NotFoundError({ message: `CA with ID '${caId}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
|
||||
@@ -1455,13 +1474,14 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
ca.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
|
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
|
||||
import { ProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -67,13 +67,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
message: `CA with ID ${caId} not found`
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -128,13 +129,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -185,13 +187,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
@@ -252,13 +255,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -336,13 +340,14 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -49,13 +50,14 @@ export const certificateServiceFactory = ({
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
@@ -72,13 +74,14 @@ export const certificateServiceFactory = ({
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
@@ -106,13 +109,14 @@ export const certificateServiceFactory = ({
|
||||
const cert = await certificateDAL.findOne({ serialNumber });
|
||||
const ca = await certificateAuthorityDAL.findById(cert.caId);
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -14,24 +15,33 @@ import {
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
|
||||
type TCmekServiceFactoryDep = {
|
||||
kmsService: TKmsServiceFactory;
|
||||
kmsDAL: TKmsKeyDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
|
||||
};
|
||||
|
||||
export type TCmekServiceFactory = ReturnType<typeof cmekServiceFactory>;
|
||||
|
||||
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TCmekServiceFactoryDep) => {
|
||||
const createCmek = async ({ projectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, projectDAL }: TCmekServiceFactoryDep) => {
|
||||
const createCmek = async ({ projectId: preSplitProjectId, ...dto }: TCreateCmekDTO, actor: OrgServiceActor) => {
|
||||
let projectId = preSplitProjectId;
|
||||
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS);
|
||||
if (cmekProjectFromSplit) {
|
||||
projectId = cmekProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
|
||||
|
||||
const cmek = await kmsService.generateKmsKey({
|
||||
@@ -50,13 +60,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
|
||||
|
||||
@@ -72,13 +83,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
|
||||
|
||||
@@ -87,7 +99,16 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
return cmek;
|
||||
};
|
||||
|
||||
const listCmeksByProjectId = async ({ projectId, ...filters }: TListCmeksByProjectIdDTO, actor: OrgServiceActor) => {
|
||||
const listCmeksByProjectId = async (
|
||||
{ projectId: preSplitProjectId, ...filters }: TListCmeksByProjectIdDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
let projectId = preSplitProjectId;
|
||||
const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(preSplitProjectId, ProjectType.KMS);
|
||||
if (cmekProjectFromSplit) {
|
||||
projectId = cmekProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
@@ -112,7 +133,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
@@ -120,6 +141,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Encrypt, ProjectPermissionSub.Cmek);
|
||||
|
||||
const encrypt = await kmsService.encryptWithKmsKey({ kmsId: keyId });
|
||||
@@ -138,13 +160,14 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
|
||||
|
||||
if (key.isDisabled) throw new BadRequestError({ message: "Key is disabled" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
key.projectId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.KMS);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Decrypt, ProjectPermissionSub.Cmek);
|
||||
|
||||
|
@@ -102,6 +102,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
|
||||
db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project),
|
||||
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
@@ -126,7 +127,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
projectId,
|
||||
projectName
|
||||
projectName,
|
||||
projectType
|
||||
}) => ({
|
||||
id,
|
||||
identityId,
|
||||
@@ -147,7 +149,8 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
},
|
||||
project: {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
name: projectName,
|
||||
type: projectType
|
||||
}
|
||||
}),
|
||||
key: "id",
|
||||
|
@@ -4,7 +4,13 @@ import { Octokit } from "@octokit/rest";
|
||||
import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client";
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
|
||||
import {
|
||||
ProjectType,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
TIntegrationAuths,
|
||||
TIntegrationAuthsInsert
|
||||
} from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -145,13 +151,14 @@ export const integrationAuthServiceFactory = ({
|
||||
if (!Object.values(Integrations).includes(integration as Integrations))
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
const tokenExchange = await exchangeCode({ integration, code, url, installationId });
|
||||
@@ -254,13 +261,14 @@ export const integrationAuthServiceFactory = ({
|
||||
if (!Object.values(Integrations).includes(integration as Integrations))
|
||||
throw new BadRequestError({ message: "Invalid integration" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
const updateDoc: TIntegrationAuthsInsert = {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
@@ -80,13 +81,14 @@ export const integrationServiceFactory = ({
|
||||
if (!integrationAuth)
|
||||
throw new NotFoundError({ message: `Integration auth with ID '${integrationAuthId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integrationAuth.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -158,13 +160,14 @@ export const integrationServiceFactory = ({
|
||||
const integration = await integrationDAL.findById(id);
|
||||
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
|
||||
const newEnvironment = environment || integration.environment.slug;
|
||||
@@ -293,13 +296,14 @@ export const integrationServiceFactory = ({
|
||||
const integration = await integrationDAL.findById(id);
|
||||
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
integration.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||
|
||||
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
|
||||
|
@@ -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 || "all");
|
||||
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,3 +1,4 @@
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type";
|
||||
@@ -55,6 +56,7 @@ export type TFindAllWorkspacesDTO = {
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
orgId: string;
|
||||
type?: ProjectType;
|
||||
};
|
||||
|
||||
export type TUpdateOrgDTO = {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -8,6 +9,7 @@ import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-colle
|
||||
import { pkiItemTypeToNameMap } from "@app/services/pki-collection/pki-collection-types";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TPkiAlertDALFactory } from "./pki-alert-dal";
|
||||
import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types";
|
||||
|
||||
@@ -19,6 +21,7 @@ type TPkiAlertServiceFactoryDep = {
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
|
||||
};
|
||||
|
||||
export type TPkiAlertServiceFactory = ReturnType<typeof pkiAlertServiceFactory>;
|
||||
@@ -27,7 +30,8 @@ export const pkiAlertServiceFactory = ({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
smtpService,
|
||||
projectDAL
|
||||
}: TPkiAlertServiceFactoryDep) => {
|
||||
const sendPkiItemExpiryNotices = async () => {
|
||||
const allAlertItems = await pkiAlertDAL.getExpiringPkiCollectionItemsForAlerting();
|
||||
@@ -63,7 +67,7 @@ export const pkiAlertServiceFactory = ({
|
||||
};
|
||||
|
||||
const createPkiAlert = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
name,
|
||||
pkiCollectionId,
|
||||
alertBeforeDays,
|
||||
@@ -73,13 +77,23 @@ export const pkiAlertServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateAlertDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
@@ -128,13 +142,14 @@ export const pkiAlertServiceFactory = ({
|
||||
let alert = await pkiAlertDAL.findById(alertId);
|
||||
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
alert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
@@ -160,13 +175,14 @@ export const pkiAlertServiceFactory = ({
|
||||
let alert = await pkiAlertDAL.findById(alertId);
|
||||
if (!alert) throw new NotFoundError({ message: `Alert with ID '${alertId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
alert.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
|
||||
alert = await pkiAlertDAL.deleteById(alertId);
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TPkiCollectionItems } from "@app/db/schemas";
|
||||
import { ProjectType, TPkiCollectionItems } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TPkiCollectionDALFactory } from "./pki-collection-dal";
|
||||
import { transformPkiCollectionItem } from "./pki-collection-fns";
|
||||
import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal";
|
||||
@@ -30,6 +31,7 @@ type TPkiCollectionServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findOne">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId">;
|
||||
};
|
||||
|
||||
export type TPkiCollectionServiceFactory = ReturnType<typeof pkiCollectionServiceFactory>;
|
||||
@@ -39,24 +41,35 @@ export const pkiCollectionServiceFactory = ({
|
||||
pkiCollectionItemDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
projectDAL
|
||||
}: TPkiCollectionServiceFactoryDep) => {
|
||||
const createPkiCollection = async ({
|
||||
name,
|
||||
description,
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreatePkiCollectionDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -106,13 +119,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
|
||||
pkiCollection = await pkiCollectionDAL.updateById(collectionId, {
|
||||
@@ -133,13 +147,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
let pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
@@ -205,13 +220,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
|
||||
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${collectionId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -298,13 +314,14 @@ export const pkiCollectionServiceFactory = ({
|
||||
|
||||
if (!pkiCollectionItem) throw new NotFoundError({ message: `PKI collection item with ID '${itemId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
pkiCollection.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -41,13 +42,14 @@ export const projectEnvServiceFactory = ({
|
||||
name,
|
||||
slug
|
||||
}: TCreateEnvDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||
|
||||
const lock = await keyStore
|
||||
@@ -129,13 +131,14 @@ export const projectEnvServiceFactory = ({
|
||||
id,
|
||||
position
|
||||
}: TUpdateEnvDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||
|
||||
const lock = await keyStore
|
||||
@@ -192,13 +195,14 @@ export const projectEnvServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||
|
||||
const lock = await keyStore
|
||||
|
@@ -217,20 +217,33 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project)
|
||||
db.ref("id").as("projectId").withSchema(TableName.Project),
|
||||
db.ref("type").as("projectType").withSchema(TableName.Project)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, projectId, projectName }) => ({
|
||||
parentMapper: ({
|
||||
email,
|
||||
firstName,
|
||||
username,
|
||||
lastName,
|
||||
publicKey,
|
||||
isGhost,
|
||||
id,
|
||||
projectId,
|
||||
projectName,
|
||||
projectType
|
||||
}) => ({
|
||||
id,
|
||||
userId,
|
||||
projectId,
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
||||
project: {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
name: projectName,
|
||||
type: projectType
|
||||
}
|
||||
}),
|
||||
key: "id",
|
||||
|
@@ -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 | "all") => {
|
||||
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 !== "all") {
|
||||
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,17 @@ 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`)
|
||||
.andWhere((qb) => {
|
||||
if (projectType) {
|
||||
void qb.where(`${TableName.Project}.type`, projectType);
|
||||
}
|
||||
})
|
||||
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.Project),
|
||||
@@ -315,6 +336,22 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getProjectFromSplitId = async (projectId: string, projectType: ProjectType) => {
|
||||
try {
|
||||
const project = await db(TableName.ProjectSplitBackfillIds)
|
||||
.where({
|
||||
sourceProjectId: projectId,
|
||||
destinationProjectType: projectType
|
||||
})
|
||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectSplitBackfillIds}.destinationProjectId`)
|
||||
.select(selectAllTableCols(TableName.Project))
|
||||
.first();
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `Failed to find split project with id ${projectId}` });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...projectOrm,
|
||||
findAllProjects,
|
||||
@@ -325,6 +362,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
findProjectByFilter,
|
||||
findProjectBySlug,
|
||||
findProjectWithOrg,
|
||||
checkProjectUpgradeStatus
|
||||
checkProjectUpgradeStatus,
|
||||
getProjectFromSplitId
|
||||
};
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { ProjectMembershipRole, ProjectVersion, TProjectEnvironments } from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectType, ProjectVersion, TProjectEnvironments } 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";
|
||||
@@ -153,10 +153,10 @@ export const projectServiceFactory = ({
|
||||
kmsKeyId,
|
||||
tx: trx,
|
||||
createDefaultEnvs = true,
|
||||
template = InfisicalProjectTemplate.Default
|
||||
template = InfisicalProjectTemplate.Default,
|
||||
type = ProjectType.SecretManager
|
||||
}: TCreateProjectDTO) => {
|
||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||
|
||||
const { permission, membership: orgMembership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -206,6 +206,7 @@ export const projectServiceFactory = ({
|
||||
const project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
type,
|
||||
description: workspaceDescription,
|
||||
orgId: organization.id,
|
||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||
@@ -430,8 +431,14 @@ 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 = ProjectType.SecretManager
|
||||
}: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
||||
|
||||
if (includeRoles) {
|
||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
||||
@@ -681,11 +688,19 @@ export const projectServiceFactory = ({
|
||||
actor
|
||||
}: TListProjectCasDTO) => {
|
||||
const project = await projectDAL.findProjectByFilter(filter);
|
||||
let projectId = project.id;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
@@ -697,7 +712,7 @@ export const projectServiceFactory = ({
|
||||
|
||||
const cas = await certificateAuthorityDAL.find(
|
||||
{
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
...(status && { status }),
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
@@ -723,18 +738,27 @@ export const projectServiceFactory = ({
|
||||
actor
|
||||
}: TListProjectCertsDTO) => {
|
||||
const project = await projectDAL.findProjectByFilter(filter);
|
||||
let projectId = project.id;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
|
||||
const cas = await certificateAuthorityDAL.find({ projectId: project.id });
|
||||
const cas = await certificateAuthorityDAL.find({ projectId });
|
||||
|
||||
const certificates = await certificateDAL.find(
|
||||
{
|
||||
@@ -748,7 +772,7 @@ export const projectServiceFactory = ({
|
||||
);
|
||||
|
||||
const count = await certificateDAL.countCertificatesInProject({
|
||||
projectId: project.id,
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
});
|
||||
@@ -763,19 +787,29 @@ export const projectServiceFactory = ({
|
||||
* Return list of (PKI) alerts configured for project
|
||||
*/
|
||||
const listProjectAlerts = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListProjectAlertsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
||||
|
||||
@@ -790,19 +824,28 @@ export const projectServiceFactory = ({
|
||||
* Return list of PKI collections for project
|
||||
*/
|
||||
const listProjectPkiCollections = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TListProjectAlertsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
||||
|
||||
@@ -817,19 +860,29 @@ export const projectServiceFactory = ({
|
||||
* Return list of certificate templates for project
|
||||
*/
|
||||
const listProjectCertificateTemplates = async ({
|
||||
projectId,
|
||||
projectId: preSplitProjectId,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actor
|
||||
}: TListProjectCertificateTemplatesDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
let projectId = preSplitProjectId;
|
||||
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
|
||||
projectId,
|
||||
ProjectType.CertificateManager
|
||||
);
|
||||
if (certManagerProjectFromSplit) {
|
||||
projectId = certManagerProjectFromSplit.id;
|
||||
}
|
||||
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.CertificateManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TProjectKeys } from "@app/db/schemas";
|
||||
import { ProjectType, TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@@ -35,6 +35,7 @@ export type TCreateProjectDTO = {
|
||||
createDefaultEnvs?: boolean;
|
||||
template?: string;
|
||||
tx?: Knex;
|
||||
type?: ProjectType;
|
||||
};
|
||||
|
||||
export type TDeleteProjectBySlugDTO = {
|
||||
@@ -84,6 +85,7 @@ export type TDeleteProjectDTO = {
|
||||
|
||||
export type TListProjectsDTO = {
|
||||
includeRoles: boolean;
|
||||
type?: ProjectType | "all";
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpgradeProjectDTO = {
|
||||
|
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
|
||||
|
||||
import { TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { ProjectType, TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
@@ -52,13 +52,14 @@ export const secretFolderServiceFactory = ({
|
||||
environment,
|
||||
path: secretPath
|
||||
}: TCreateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -150,13 +151,14 @@ export const secretFolderServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
folders.forEach(({ environment, path: secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -259,13 +261,14 @@ export const secretFolderServiceFactory = ({
|
||||
path: secretPath,
|
||||
id
|
||||
}: TUpdateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
@@ -339,13 +342,14 @@ export const secretFolderServiceFactory = ({
|
||||
path: secretPath,
|
||||
idOrName
|
||||
}: TDeleteFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
||||
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ProjectType, TableName } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -73,13 +73,14 @@ export const secretImportServiceFactory = ({
|
||||
isReplication,
|
||||
path: secretPath
|
||||
}: TCreateSecretImportDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
// check if user has permission to import into destination path
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -189,13 +190,15 @@ export const secretImportServiceFactory = ({
|
||||
data,
|
||||
id
|
||||
}: TUpdateSecretImportDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
@@ -283,13 +286,15 @@ export const secretImportServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
id
|
||||
}: TDeleteSecretImportDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -23,7 +24,7 @@ export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory
|
||||
|
||||
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
|
||||
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -31,6 +32,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const existingTag = await secretTagDAL.findOne({ slug, projectId });
|
||||
if (existingTag) throw new BadRequestError({ message: "Tag already exist" });
|
||||
@@ -54,7 +56,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
tag.projectId,
|
||||
@@ -62,6 +64,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
|
||||
return updatedTag;
|
||||
@@ -71,7 +74,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
const tag = await secretTagDAL.findById(id);
|
||||
if (!tag) throw new NotFoundError({ message: `Tag with ID '${id}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
tag.projectId,
|
||||
@@ -79,6 +82,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const deletedTag = await secretTagDAL.deleteById(tag.id);
|
||||
return deletedTag;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError, PureAbility, subject } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectType, SecretsV2Schema, SecretType, TableName } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
@@ -188,13 +188,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
...inputSecret
|
||||
}: TCreateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -310,13 +311,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
...inputSecret
|
||||
}: TUpdateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
if (inputSecret.newSecretName === "") {
|
||||
throw new BadRequestError({ message: "New secret name cannot be empty" });
|
||||
@@ -494,13 +496,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
...inputSecret
|
||||
}: TDeleteSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1081,13 +1084,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectId,
|
||||
secrets: inputSecrets
|
||||
}: TCreateManySecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1221,13 +1225,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretPath,
|
||||
secrets: inputSecrets
|
||||
}: TUpdateManySecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1427,13 +1432,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteManySecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
@@ -1569,13 +1575,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TBackFillSecretReferencesDTO) => {
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
const { hasRole, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
if (!hasRole(ProjectMembershipRole.Admin))
|
||||
throw new ForbiddenRequestError({ message: "Only admins are allowed to take this action" });
|
||||
@@ -1616,13 +1623,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TMoveSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
const sourceFolder = await folderDAL.findBySecretPath(projectId, sourceEnvironment, sourceSecretPath);
|
||||
if (!sourceFolder) {
|
||||
|
@@ -4,6 +4,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
ProjectType,
|
||||
ProjectUpgradeStatus,
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
@@ -186,13 +187,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
...inputSecret
|
||||
}: TCreateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -301,13 +304,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
...inputSecret
|
||||
}: TUpdateSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -443,13 +448,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
...inputSecret
|
||||
}: TDeleteSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -732,13 +739,14 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
secrets: inputSecrets
|
||||
}: TCreateBulkSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -817,13 +825,15 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
secrets: inputSecrets
|
||||
}: TUpdateBulkSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
@@ -923,13 +933,14 @@ export const secretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteBulkSecretDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
|
@@ -41,6 +41,30 @@ The operator can be install via [Helm](https://helm.sh) or [kubectl](https://git
|
||||
helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0
|
||||
```
|
||||
|
||||
**Namespace-scoped Installation**
|
||||
|
||||
The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access.
|
||||
|
||||
```bash
|
||||
helm install operator infisical-helm-charts/secrets-operator \
|
||||
--namespace your-namespace \
|
||||
--set scopedNamespace=your-namespace \
|
||||
--set scopedRBAC=true
|
||||
```
|
||||
|
||||
When scoped to a namespace, the operator will:
|
||||
|
||||
- Only watch InfisicalSecrets in the specified namespace
|
||||
- Only create/update Kubernetes secrets in that namespace
|
||||
- Only access deployments in that namespace
|
||||
|
||||
The default configuration gives cluster-wide access:
|
||||
|
||||
```yaml
|
||||
scopedNamespace: "" # Empty for cluster-wide access
|
||||
scopedRBAC: false # Cluster-wide permissions
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubectl">
|
||||
For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version.
|
||||
|
@@ -39,7 +39,7 @@ Used to configure platform-specific security and operational settings
|
||||
The platform utilizes Postgres to persist all of its data and Redis for caching and backgroud tasks
|
||||
|
||||
<ParamField query="DB_CONNECTION_URI" type="string" default="" required>
|
||||
Postgres database connection string.
|
||||
Postgres database connection string. The format generally looks like this: `postgresql://username:password@host:5432/database`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="DB_ROOT_CERT" type="string" default="" optional>
|
||||
@@ -49,7 +49,7 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="REDIS_URL" type="string" default="none" required>
|
||||
Redis connection string.
|
||||
Redis connection string. The format generally looks like this: `redis://host:6379`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="DB_READ_REPLICAS" type="string" default="" optional>
|
||||
|
808
frontend/public/lotties/note.json
Normal file
808
frontend/public/lotties/note.json
Normal file
@@ -0,0 +1,808 @@
|
||||
{
|
||||
"v": "5.12.2",
|
||||
"fr": 29.9700012207031,
|
||||
"ip": 0,
|
||||
"op": 45.0000018328876,
|
||||
"w": 48,
|
||||
"h": 48,
|
||||
"nm": "note",
|
||||
"ddd": 1,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 1,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "note-outline-bot_s1g1_s2g2_s3g1_s4g1 Outlines",
|
||||
"parent": 2,
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"rx": { "a": 0, "k": 0, "ix": 8 },
|
||||
"ry": { "a": 0, "k": 0, "ix": 9 },
|
||||
"rz": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{ "i": { "x": [0], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 0, "s": [0] },
|
||||
{ "i": { "x": [0], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 22, "s": [5] },
|
||||
{ "t": 44.0000017921567, "s": [0] }
|
||||
],
|
||||
"ix": 10
|
||||
},
|
||||
"or": { "a": 0, "k": [0, 0, 0], "ix": 7 },
|
||||
"p": { "a": 0, "k": [19.448, 27.122, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [13.405, 11.539, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.022, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5, 18.078],
|
||||
[21.809, 18.078]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.022, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 22,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[4.311, 17.73],
|
||||
[21.654, 16.837]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 44.0000017921567,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5, 18.078],
|
||||
[21.809, 18.078]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ind": 1,
|
||||
"ty": "sh",
|
||||
"ix": 2,
|
||||
"ks": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.022, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5, 11.281],
|
||||
[21.809, 11.281]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.022, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 22,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5, 11.281],
|
||||
[21.735, 9.907]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 44.0000017921567,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5, 11.281],
|
||||
[21.809, 11.281]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 2",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ind": 2,
|
||||
"ty": "sh",
|
||||
"ix": 3,
|
||||
"ks": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.022, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5, 5],
|
||||
[13.756, 5]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.022, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 22,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5.497, 4.939],
|
||||
[14.633, 4.166]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 44.0000017921567,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[5, 5],
|
||||
[13.756, 5]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 3",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "mm",
|
||||
"mm": 1,
|
||||
"nm": "Merge Paths 1",
|
||||
"mn": "ADBE Vector Filter - Merge",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 3 },
|
||||
"o": { "a": 0, "k": 100, "ix": 4 },
|
||||
"w": { "a": 0, "k": 2.5, "ix": 5 },
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "Stroke 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 5,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tm",
|
||||
"s": { "a": 0, "k": 0, "ix": 1 },
|
||||
"e": { "a": 0, "k": 100, "ix": 2 },
|
||||
"o": { "a": 0, "k": 0, "ix": 3 },
|
||||
"m": 1,
|
||||
"ix": 2,
|
||||
"nm": "Trim Paths 1",
|
||||
"mn": "ADBE Vector Filter - Trim",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tm",
|
||||
"s": { "a": 0, "k": 0, "ix": 1 },
|
||||
"e": { "a": 0, "k": 100, "ix": 2 },
|
||||
"o": { "a": 0, "k": 0, "ix": 3 },
|
||||
"m": 1,
|
||||
"ix": 3,
|
||||
"nm": "Trim Paths 2",
|
||||
"mn": "ADBE Vector Filter - Trim",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ty": "tm",
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0.616], "y": [1] },
|
||||
"o": { "x": [0.41], "y": [0] },
|
||||
"t": 0,
|
||||
"s": [0]
|
||||
},
|
||||
{
|
||||
"i": { "x": [0.005], "y": [1] },
|
||||
"o": { "x": [0.369], "y": [0] },
|
||||
"t": 22,
|
||||
"s": [100]
|
||||
},
|
||||
{ "t": 44.0000017921567, "s": [0] }
|
||||
],
|
||||
"ix": 1
|
||||
},
|
||||
"e": { "a": 0, "k": 100, "ix": 2 },
|
||||
"o": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": [0], "y": [1] },
|
||||
"o": { "x": [0.333], "y": [0] },
|
||||
"t": 22,
|
||||
"s": [0]
|
||||
},
|
||||
{ "t": 44.0000017921567, "s": [360] }
|
||||
],
|
||||
"ix": 3
|
||||
},
|
||||
"m": 1,
|
||||
"ix": 1,
|
||||
"nm": "Trim Paths 1",
|
||||
"mn": "ADBE Vector Filter - Trim",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 2",
|
||||
"np": 1,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 4,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 45.0000018328876,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 1,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "note-outline-bot_s1g1_s2g1_s3g1_s4g1_background Outlines",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"rx": { "a": 0, "k": 0, "ix": 8 },
|
||||
"ry": { "a": 0, "k": 0, "ix": 9 },
|
||||
"rz": { "a": 0, "k": 0, "ix": 10 },
|
||||
"or": { "a": 0, "k": [0, 0, 0], "ix": 7 },
|
||||
"p": { "a": 0, "k": [24.778, 23.858, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [19.825, 23.313, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[1.423, 1.657],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, -2.354],
|
||||
[0, 0],
|
||||
[-1.324, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[-1.482, -1.727],
|
||||
[0, 0],
|
||||
[0, 1.429],
|
||||
[0, 0],
|
||||
[2.093, 0]
|
||||
],
|
||||
"v": [
|
||||
[4.581, 2.516],
|
||||
[1.876, -0.634],
|
||||
[-1.99, -5.137],
|
||||
[-6.005, -3.212],
|
||||
[-6.005, 4.277],
|
||||
[-3.606, 6.865],
|
||||
[2.822, 6.865]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 22,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0.56, 0.711],
|
||||
[0.835, 1.361],
|
||||
[0, 0],
|
||||
[-0.031, 0.137],
|
||||
[0, 0],
|
||||
[-0.118, -0.836],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[-0.835, -1.361],
|
||||
[-0.299, -0.286],
|
||||
[0, 0],
|
||||
[1.062, 0.814],
|
||||
[0, 0],
|
||||
[-0.101, 0.125]
|
||||
],
|
||||
"v": [
|
||||
[6.281, -2.819],
|
||||
[5.393, -4.55],
|
||||
[3.663, -5.916],
|
||||
[3.337, -6.047],
|
||||
[4.119, -5.661],
|
||||
[6.298, -2.792],
|
||||
[6.5, -1.722]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 44.0000017921567,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[1.423, 1.657],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, -2.354],
|
||||
[0, 0],
|
||||
[-1.324, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[-1.482, -1.727],
|
||||
[0, 0],
|
||||
[0, 1.429],
|
||||
[0, 0],
|
||||
[2.093, 0]
|
||||
],
|
||||
"v": [
|
||||
[4.581, 2.516],
|
||||
[1.876, -0.634],
|
||||
[-1.99, -5.137],
|
||||
[-6.005, -3.212],
|
||||
[-6.005, 4.277],
|
||||
[-3.606, 6.865],
|
||||
[2.822, 6.865]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 3 },
|
||||
"o": { "a": 0, "k": 100, "ix": 4 },
|
||||
"w": { "a": 0, "k": 2.5, "ix": 5 },
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "Stroke 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 4 },
|
||||
"o": { "a": 0, "k": 0, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [28.646, 11.865], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 3,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, -0.781],
|
||||
[0, 0],
|
||||
[2.763, 0],
|
||||
[0, 0],
|
||||
[0, 2.828],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[-2.763, 0],
|
||||
[0, 0],
|
||||
[-0.625, -0.733]
|
||||
],
|
||||
"o": [
|
||||
[0.507, 0.595],
|
||||
[0, 0],
|
||||
[0, 2.828],
|
||||
[0, 0],
|
||||
[-2.763, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, -2.828],
|
||||
[0, 0],
|
||||
[0.963, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[14.012, -8.411],
|
||||
[14.797, -6.279],
|
||||
[14.797, 13.05],
|
||||
[9.794, 18.171],
|
||||
[-9.794, 18.171],
|
||||
[-14.796, 13.05],
|
||||
[-14.796, 0.99],
|
||||
[-14.796, -13.051],
|
||||
[-9.794, -18.171],
|
||||
[4.174, -18.171],
|
||||
[6.676, -17.016]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 22,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0.171, -6.028],
|
||||
[0, 0],
|
||||
[2.763, 0],
|
||||
[0, 0],
|
||||
[0, 2.828],
|
||||
[-1.453, 6.385],
|
||||
[0, 0],
|
||||
[-2.763, 0],
|
||||
[0, 0],
|
||||
[-1.739, -0.297]
|
||||
],
|
||||
"o": [
|
||||
[0.051, 2.426],
|
||||
[-0.171, 6.028],
|
||||
[-1.046, 2.979],
|
||||
[0, 0],
|
||||
[-2.763, 0],
|
||||
[0, 0],
|
||||
[1.266, -5.562],
|
||||
[0, -2.828],
|
||||
[0, 0],
|
||||
[0.963, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[15.322, -13.817],
|
||||
[14.796, -0.028],
|
||||
[12.547, 13.021],
|
||||
[6.669, 18.201],
|
||||
[-12.044, 18.141],
|
||||
[-17.046, 13.021],
|
||||
[-14.421, 1.025],
|
||||
[-12.923, -12.566],
|
||||
[-7.92, -17.686],
|
||||
[3.673, -17.672],
|
||||
[12.612, -17.453]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 44.0000017921567,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, -0.781],
|
||||
[0, 0],
|
||||
[2.763, 0],
|
||||
[0, 0],
|
||||
[0, 2.828],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[-2.763, 0],
|
||||
[0, 0],
|
||||
[-0.625, -0.733]
|
||||
],
|
||||
"o": [
|
||||
[0.507, 0.595],
|
||||
[0, 0],
|
||||
[0, 2.828],
|
||||
[0, 0],
|
||||
[-2.763, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, -2.828],
|
||||
[0, 0],
|
||||
[0.963, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[14.012, -8.411],
|
||||
[14.797, -6.279],
|
||||
[14.797, 13.05],
|
||||
[9.794, 18.171],
|
||||
[-9.794, 18.171],
|
||||
[-14.796, 13.05],
|
||||
[-14.796, 0.99],
|
||||
[-14.796, -13.051],
|
||||
[-9.794, -18.171],
|
||||
[4.174, -18.171],
|
||||
[6.676, -17.016]
|
||||
],
|
||||
"c": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 3 },
|
||||
"o": { "a": 0, "k": 100, "ix": 4 },
|
||||
"w": { "a": 0, "k": 2.5, "ix": 5 },
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "Stroke 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 4 },
|
||||
"o": { "a": 0, "k": 0, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [19.797, 23.455], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 2",
|
||||
"np": 3,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 2,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 45.0000018328876,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": [],
|
||||
"props": {}
|
||||
}
|
470
frontend/public/lotties/unlock.json
Normal file
470
frontend/public/lotties/unlock.json
Normal file
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"v": "5.12.2",
|
||||
"fr": 29.9700012207031,
|
||||
"ip": 0,
|
||||
"op": 45.0000018328876,
|
||||
"w": 48,
|
||||
"h": 48,
|
||||
"nm": "unlock",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "unlock-outline-top_s1g1_s2g2_s3g1_s4g1_background Outlines",
|
||||
"parent": 2,
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{ "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 12, "s": [0] },
|
||||
{
|
||||
"i": { "x": [0.667], "y": [1] },
|
||||
"o": { "x": [0.333], "y": [0] },
|
||||
"t": 28,
|
||||
"s": [-16]
|
||||
},
|
||||
{ "t": 40.0000016292334, "s": [0] }
|
||||
],
|
||||
"ix": 10
|
||||
},
|
||||
"p": { "a": 0, "k": [19, 19.473, 0], "ix": 2, "l": 2 },
|
||||
"a": { "a": 0, "k": [8.5, 11.125, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, 1.292],
|
||||
[1.933, 0],
|
||||
[0, -1.933],
|
||||
[-1.042, -0.607],
|
||||
[0, 0],
|
||||
[-0.966, 0],
|
||||
[0, 0.966],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, -1.933],
|
||||
[-1.933, 0],
|
||||
[0, 1.292],
|
||||
[0, 0],
|
||||
[0, 0.966],
|
||||
[0.966, 0],
|
||||
[0, 0],
|
||||
[1.042, -0.607]
|
||||
],
|
||||
"v": [
|
||||
[3.5, -2.625],
|
||||
[0, -6.125],
|
||||
[-3.5, -2.625],
|
||||
[-1.75, 0.39],
|
||||
[-1.75, 4.375],
|
||||
[0, 6.125],
|
||||
[1.75, 4.375],
|
||||
[1.75, 0.39]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 3 },
|
||||
"o": { "a": 0, "k": 100, "ix": 4 },
|
||||
"w": { "a": 0, "k": 2.5, "ix": 5 },
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "Stroke 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 4 },
|
||||
"o": { "a": 0, "k": 0, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [8.5, 11.125], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 3,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 45.0000018328876,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "unlock-outline-bot_s1g1_s2g1_s3g1_s4g1_background Outlines",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [24, 29.826, 0],
|
||||
"to": [0, 0.313, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 12,
|
||||
"s": [24, 31.701, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [0, 0.313, 0]
|
||||
},
|
||||
{ "t": 28.0000011404634, "s": [24, 29.826, 0] }
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": { "a": 0, "k": [19, 19.523, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[-1.43, 0.576],
|
||||
[0, 0],
|
||||
[-3.266, -1.28],
|
||||
[0, 0],
|
||||
[0, -1.555],
|
||||
[0, 0],
|
||||
[7.732, 0],
|
||||
[0, 8.353],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[3.253, -1.31],
|
||||
[0, 0],
|
||||
[1.448, 0.567],
|
||||
[0, 0],
|
||||
[0, 8.353],
|
||||
[-7.732, 0],
|
||||
[0, 0],
|
||||
[0, -1.542]
|
||||
],
|
||||
"v": [
|
||||
[-11.633, -10.628],
|
||||
[-5.258, -13.195],
|
||||
[4.892, -13.243],
|
||||
[11.6, -10.615],
|
||||
[14, -7.097],
|
||||
[14, -0.602],
|
||||
[0, 14.523],
|
||||
[-14, -0.602],
|
||||
[-14, -7.123]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 3 },
|
||||
"o": { "a": 0, "k": 100, "ix": 4 },
|
||||
"w": { "a": 0, "k": 2.5, "ix": 5 },
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "Stroke 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 4 },
|
||||
"o": { "a": 0, "k": 0, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [19, 19.523], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 3,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 45.0000018328876,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 3,
|
||||
"ty": 4,
|
||||
"nm": "unlockoutline-bot_s1g1_s2g1_s3g1_s4g2 Outlines",
|
||||
"parent": 2,
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{ "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 0, "s": [0] },
|
||||
{ "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 12, "s": [9] },
|
||||
{ "i": { "x": [0.667], "y": [1] }, "o": { "x": [0.333], "y": [0] }, "t": 28, "s": [9] },
|
||||
{ "t": 40.0000016292334, "s": [0] }
|
||||
],
|
||||
"ix": 10
|
||||
},
|
||||
"p": { "a": 0, "k": [17.628, 0.002, 0], "ix": 2, "l": 2 },
|
||||
"a": { "a": 0, "k": [15.029, 13.585, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[5.305, -0.847],
|
||||
[-0.847, -5.305],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[-0.847, -5.305],
|
||||
[-5.305, 0.846],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[10.029, 0.334],
|
||||
[-1.11, -7.738],
|
||||
[-9.182, 3.401],
|
||||
[-8.355, 8.585]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 12,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[5.341, -0.581],
|
||||
[-0.582, -5.341],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[-0.582, -5.341],
|
||||
[-5.341, 0.581],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[11.833, 5.021],
|
||||
[1.11, -3.596],
|
||||
[-7.507, 7.127],
|
||||
[-7.094, 9.966]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 28,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[5.341, -0.581],
|
||||
[-0.582, -5.341],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[-0.582, -5.341],
|
||||
[-5.341, 0.581],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[11.833, 5.021],
|
||||
[1.11, -3.596],
|
||||
[-7.507, 7.127],
|
||||
[-7.094, 9.966]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 40.0000016292334,
|
||||
"s": [
|
||||
{
|
||||
"i": [
|
||||
[0, 0],
|
||||
[5.305, -0.847],
|
||||
[-0.847, -5.305],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[-0.847, -5.305],
|
||||
[-5.305, 0.846],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[10.029, 0.334],
|
||||
[-1.11, -7.738],
|
||||
[-9.182, 3.401],
|
||||
[-8.355, 8.585]
|
||||
],
|
||||
"c": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": { "a": 0, "k": [1, 1, 1], "ix": 3 },
|
||||
"o": { "a": 0, "k": 100, "ix": 4 },
|
||||
"w": { "a": 0, "k": 2.5, "ix": 5 },
|
||||
"lc": 2,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "Stroke 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [15.029, 13.585], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 45.0000018328876,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": [],
|
||||
"props": {}
|
||||
}
|
@@ -5,6 +5,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Tag } from "public/data/frequentInterfaces";
|
||||
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
/**
|
||||
* This is the menu that is used to add more tags to a secret
|
||||
* @param {object} obj
|
||||
@@ -75,7 +77,7 @@ const AddTagsMenu = ({
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-sm bg-mineshaft-800 px-2 py-0.5 text-left text-bunker-200 duration-200 hover:bg-primary hover:text-black"
|
||||
onClick={() => router.push(`/project/${String(router.query.id)}/settings`)}
|
||||
onClick={() => router.push(`/${ProjectType.SecretManager}/${String(router.query.id)}/settings`)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-2 text-xs" />
|
||||
Add more tags
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { Button } from "../v2";
|
||||
|
||||
interface IProps {
|
||||
@@ -20,7 +22,7 @@ export const NoEnvironmentsBanner = ({ projectId }: IProps) => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button onClick={() => router.push(`/project/${projectId}/settings#environments`)}>
|
||||
<Button onClick={() => router.push(`/${ProjectType.SecretManager}/${projectId}/settings#environments`)}>
|
||||
Add environments
|
||||
</Button>
|
||||
</div>
|
||||
|
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { useAddUsersToOrg } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { Button, EmailServiceSetupModal } from "../v2";
|
||||
@@ -22,7 +23,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
|
||||
// Redirect user to the getting started page
|
||||
const redirectToHome = async () => {
|
||||
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
|
||||
router.push(`/org/${localStorage.getItem("orgData.id")}/${ProjectType.SecretManager}/overview`);
|
||||
};
|
||||
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
|
@@ -32,6 +32,7 @@ import {
|
||||
useSubscription,
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
@@ -41,6 +42,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 +61,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();
|
||||
@@ -117,15 +120,15 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
if (!user) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
project: { id: newProjectId }
|
||||
}
|
||||
data: { project }
|
||||
} = await createWs.mutateAsync({
|
||||
projectName: name,
|
||||
projectDescription: description,
|
||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||
template
|
||||
template,
|
||||
type: projectType
|
||||
});
|
||||
const { id: newProjectId } = project;
|
||||
|
||||
if (addMembers) {
|
||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||
@@ -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(getProjectHomePage(project));
|
||||
} 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>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type TWorkspaceContext = {
|
||||
workspaces: Workspace[];
|
||||
@@ -35,7 +35,7 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
||||
const shouldTriggerNoProjectAccess =
|
||||
!value.isLoading &&
|
||||
!value.currentWorkspace &&
|
||||
router.pathname.startsWith("/project") &&
|
||||
Object.values(ProjectType).some((el) => router.pathname.startsWith(`/${el}`)) &&
|
||||
workspaceId;
|
||||
|
||||
// handle redirects for project-specific routes
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
const secretsToBeAdded = [
|
||||
{
|
||||
@@ -36,12 +37,13 @@ const secretsToBeAdded = [
|
||||
* Create and initialize a new project in organization with id [organizationId]
|
||||
* Note: current user should be a member of the organization
|
||||
*/
|
||||
const initProjectHelper = async ({ projectName }: { projectName: string }) => {
|
||||
export const initProjectHelper = async ({ projectName }: { projectName: string }) => {
|
||||
// create new project
|
||||
const {
|
||||
data: { project }
|
||||
} = await createWorkspace({
|
||||
projectName
|
||||
projectName,
|
||||
type: ProjectType.SecretManager
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -59,4 +61,22 @@ const initProjectHelper = async ({ projectName }: { projectName: string }) => {
|
||||
return project;
|
||||
};
|
||||
|
||||
export { initProjectHelper };
|
||||
export const getProjectHomePage = (workspace: Workspace) => {
|
||||
if (workspace.type === ProjectType.SecretManager) {
|
||||
return `/${workspace.type}/${workspace.id}/secrets/overview`;
|
||||
}
|
||||
if (workspace.type === ProjectType.CertificateManager) {
|
||||
return `/${workspace.type}/${workspace.id}/certificates`;
|
||||
}
|
||||
|
||||
return `/${workspace.type}/${workspace.id}/kms`;
|
||||
};
|
||||
|
||||
export const getProjectTitle = (type: ProjectType) => {
|
||||
const titleConvert = {
|
||||
[ProjectType.SecretManager]: "Secret Management",
|
||||
[ProjectType.KMS]: "Key Management",
|
||||
[ProjectType.CertificateManager]: "Cert Management"
|
||||
};
|
||||
return titleConvert[type];
|
||||
};
|
||||
|
@@ -47,7 +47,7 @@ export type IdentityMembershipOrg = {
|
||||
export type IdentityMembership = {
|
||||
id: string;
|
||||
identity: Identity;
|
||||
project: Pick<Workspace, "id" | "name">;
|
||||
project: Pick<Workspace, "id" | "name" | "type">;
|
||||
roles: Array<
|
||||
{
|
||||
id: string;
|
||||
|
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { MfaMethod } from "../auth/types";
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
||||
import { ProjectType, ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
||||
|
||||
export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
@@ -95,6 +95,7 @@ export type TWorkspaceUser = {
|
||||
project: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProjectType;
|
||||
};
|
||||
inviteEmail: string;
|
||||
organization: string;
|
||||
|
@@ -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 | "all") => {
|
||||
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 = "all"
|
||||
}: {
|
||||
includeRoles?: boolean;
|
||||
type?: ProjectType | "all";
|
||||
} = {}) =>
|
||||
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}`, {
|
||||
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`, {
|
||||
return useMutation<Workspace, {}, ToggleAutoCapitalizationDTO>({
|
||||
mutationFn: async ({ workspaceID, state }) => {
|
||||
const { data } = await apiRequest.post<{ workspace: Workspace }>(
|
||||
`/api/v1/workspace/${workspaceID}/auto-capitalization`,
|
||||
{
|
||||
autoCapitalization: state
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
);
|
||||
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`, {
|
||||
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",
|
||||
KMS = "kms"
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
@@ -5,7 +5,7 @@
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable func-names */
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -38,21 +38,17 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useGetAccessRequestsCount,
|
||||
useGetOrgTrialUrl,
|
||||
useGetSecretApprovalRequestCount,
|
||||
useLogoutUser,
|
||||
useSelectOrganization
|
||||
} from "@app/hooks/api";
|
||||
import { useGetOrgTrialUrl, useLogoutUser, useSelectOrganization } from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { InsecureConnectionBanner } from "@app/layouts/AppLayout/components/InsecureConnectionBanner";
|
||||
import { ProjectSelect } from "@app/layouts/AppLayout/components/ProjectSelect";
|
||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||
import { Mfa } from "@app/views/Login/Mfa";
|
||||
import { CreateOrgModal } from "@app/views/Org/components";
|
||||
|
||||
import { ProjectSidebarItem } from "./components/ProjectSidebarItems";
|
||||
import { WishForm } from "./components/WishForm/WishForm";
|
||||
|
||||
interface LayoutProps {
|
||||
@@ -87,7 +83,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
const { workspaces, currentWorkspace } = useWorkspace();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { orgs, currentOrg } = useOrganization();
|
||||
|
||||
const [shouldShowMfa, toggleShowMfa] = useToggle(false);
|
||||
@@ -96,15 +92,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||
|
||||
const pendingRequestsCount = useMemo(() => {
|
||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
}, [secretApprovalReqCount, accessApprovalRequestCount]);
|
||||
|
||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||
|
||||
@@ -152,41 +139,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
if (tempLocalStorage("orgData.id") === "" && orgs?.[0]?.id) {
|
||||
localStorage.setItem("orgData.id", orgs?.[0]?.id);
|
||||
}
|
||||
|
||||
if (
|
||||
currentOrg &&
|
||||
((workspaces?.length === 0 && router.asPath.includes("project")) ||
|
||||
router.asPath.includes("/project/undefined") ||
|
||||
(!orgs?.map((org) => org.id)?.includes(router.query.id as string) &&
|
||||
!router.asPath.includes("project") &&
|
||||
!router.asPath.includes("personal") &&
|
||||
!router.asPath.includes("secret-scanning") &&
|
||||
!router.asPath.includes("integration")))
|
||||
) {
|
||||
router.push(`/org/${currentOrg?.id}/overview`);
|
||||
}
|
||||
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
||||
|
||||
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
||||
|
||||
// let intendedWorkspaceId;
|
||||
// if (pathSegments.length >= 2 && pathSegments[0] === "dashboard") {
|
||||
// [, intendedWorkspaceId] = pathSegments;
|
||||
// } else if (pathSegments.length >= 3 && pathSegments[0] === "settings") {
|
||||
// [, , intendedWorkspaceId] = pathSegments;
|
||||
// } else {
|
||||
// const lastPathSegments = router.asPath.split("/").pop();
|
||||
// if (lastPathSegments !== undefined) {
|
||||
// [intendedWorkspaceId] = lastPathSegments.split("?");
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!intendedWorkspaceId) return;
|
||||
|
||||
// if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
|
||||
// localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
// }
|
||||
// }
|
||||
};
|
||||
putUserInOrg();
|
||||
}, [router.query.id]);
|
||||
@@ -214,9 +166,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
<div>
|
||||
{!router.asPath.includes("personal") && (
|
||||
<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`}>
|
||||
{(currentWorkspace || router.asPath.includes("integrations")) && (
|
||||
<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 +330,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
|
||||
@@ -387,119 +338,46 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</Link>
|
||||
))}
|
||||
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
||||
{(router.asPath.includes("project") || router.asPath.includes("integrations")) &&
|
||||
currentWorkspace ? (
|
||||
<Menu>
|
||||
<Link href={`/project/${currentWorkspace?.id}/secrets/overview`} passHref>
|
||||
<ProjectSidebarItem />
|
||||
{router.pathname.startsWith("/org") && (
|
||||
<Menu className="mt-4">
|
||||
<Link
|
||||
href={`/org/${currentOrg?.id}/${ProjectType.SecretManager}/overview`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes(
|
||||
`/project/${currentWorkspace?.id}/secrets`
|
||||
`/${ProjectType.SecretManager}/overview`
|
||||
)}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
{t("nav.menu.secrets")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/certificates`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/certificates`
|
||||
}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Internal PKI
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/kms`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?.id}/kms`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Key Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/members`
|
||||
}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
Access Control
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/integrations/${currentWorkspace?.id}`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/integrations")}
|
||||
icon="system-outline-82-extension"
|
||||
>
|
||||
{t("nav.menu.integrations")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/secret-rotation`} passHref>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/secret-rotation`
|
||||
}
|
||||
icon="rotation"
|
||||
>
|
||||
Secret Rotation
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/approval`} passHref>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/approval`
|
||||
}
|
||||
icon="system-outline-189-domain-verification"
|
||||
>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open ||
|
||||
accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 py-0.5 px-1 text-xs font-semibold text-black">
|
||||
{pendingRequestsCount}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/settings`
|
||||
}
|
||||
icon="system-outline-109-slider-toggle-settings"
|
||||
>
|
||||
{t("nav.menu.project-settings")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
</Menu>
|
||||
) : (
|
||||
<Menu className="mt-4">
|
||||
<Link href={`/org/${currentOrg?.id}/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/overview")}
|
||||
icon="system-outline-165-view-carousel"
|
||||
>
|
||||
Overview
|
||||
Secret Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/org/${currentOrg?.id}/${ProjectType.CertificateManager}/overview`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes(
|
||||
`/${ProjectType.CertificateManager}/overview`
|
||||
)}
|
||||
icon="note"
|
||||
>
|
||||
Cert Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?.id}/${ProjectType.KMS}/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes(`/${ProjectType.KMS}/overview`)}
|
||||
icon="unlock"
|
||||
>
|
||||
Key Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
|
@@ -5,6 +5,7 @@ import { faBugs, faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
@@ -62,7 +63,7 @@ const ErrorPage = ({ error }: { error: Error | null }) => {
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
// we need to go to /org/${orgId}/overview, but we need to do a full page reload to ensure that the error the user is facing is properly reset.
|
||||
window.location.assign(`/org/${orgId}/overview`)
|
||||
window.location.assign(`/org/${orgId}/${ProjectType.SecretManager}/overview`)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHome} className="mr-2" />
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -16,10 +16,11 @@ import {
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { getProjectHomePage, getProjectTitle } from "@app/helpers/project";
|
||||
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 +139,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))
|
||||
@@ -164,7 +166,9 @@ export const ProjectSelect = () => {
|
||||
|
||||
return (
|
||||
<div className="mt-5 mb-4 w-full p-3">
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">Project</p>
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
||||
{currentWorkspace?.type ? getProjectTitle(currentWorkspace?.type) : "Project"}
|
||||
</p>
|
||||
<FilterableSelect
|
||||
className="text-sm"
|
||||
value={value}
|
||||
@@ -189,7 +193,7 @@ export const ProjectSelect = () => {
|
||||
// todo(akhi): this is not using react query because react query in overview is throwing error when envs are not exact same count
|
||||
// to reproduce change this back to router.push and switch between two projects with different env count
|
||||
// look into this on dashboard revamp
|
||||
window.location.assign(`/project/${project.id}/secrets/overview`);
|
||||
window.location.assign(getProjectHomePage(project));
|
||||
}}
|
||||
options={options}
|
||||
components={{
|
||||
@@ -206,6 +210,7 @@ export const ProjectSelect = () => {
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
projectType={currentWorkspace?.type || ProjectType.SecretManager}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@@ -0,0 +1,158 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Menu, MenuItem } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const ProjectSidebarItem = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||
|
||||
const pendingRequestsCount =
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
if (
|
||||
!currentWorkspace ||
|
||||
router.asPath.startsWith("personal") ||
|
||||
router.asPath.startsWith("integrations")
|
||||
) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager;
|
||||
const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager;
|
||||
const isCmek = currentWorkspace?.type === ProjectType.KMS;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{isSecretManager && (
|
||||
<Link
|
||||
href={`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secrets/overview`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes(
|
||||
`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secrets`
|
||||
)}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
{t("nav.menu.secrets")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isCertManager && (
|
||||
<Link
|
||||
href={`/${ProjectType.CertificateManager}/${currentWorkspace?.id}/certificates`}
|
||||
passHref
|
||||
>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath ===
|
||||
`/${ProjectType.CertificateManager}/${currentWorkspace?.id}/certificates`
|
||||
}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isCmek && (
|
||||
<Link href={`/${ProjectType.KMS}/${currentWorkspace?.id}/kms`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/${ProjectType.KMS}/${currentWorkspace?.id}/kms`}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/${currentWorkspace.type}/${currentWorkspace?.id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.endsWith(`/${currentWorkspace?.id}/members`)}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
Access Control
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
{isSecretManager && (
|
||||
<Link href={`/integrations/${currentWorkspace?.id}`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/integrations")}
|
||||
icon="system-outline-82-extension"
|
||||
>
|
||||
{t("nav.menu.integrations")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isSecretManager && (
|
||||
<Link
|
||||
href={`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secret-rotation`}
|
||||
passHref
|
||||
>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath ===
|
||||
`/${ProjectType.SecretManager}/${currentWorkspace?.id}/secret-rotation`
|
||||
}
|
||||
icon="rotation"
|
||||
>
|
||||
Secret Rotation
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{isSecretManager && (
|
||||
<Link href={`/secret-manager/${currentWorkspace?.id}/approval`} passHref>
|
||||
<a className="relative">
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/${ProjectType.SecretManager}/${currentWorkspace?.id}/approval`
|
||||
}
|
||||
icon="system-outline-189-domain-verification"
|
||||
>
|
||||
Approvals
|
||||
{Boolean(
|
||||
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
|
||||
) && (
|
||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 py-0.5 px-1 text-xs font-semibold text-black">
|
||||
{pendingRequestsCount}
|
||||
</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/${currentWorkspace.type}/${currentWorkspace?.id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.endsWith(`/${currentWorkspace?.id}/settings`)}
|
||||
icon="system-outline-109-slider-toggle-settings"
|
||||
>
|
||||
{t("nav.menu.project-settings")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
</Menu>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export { ProjectSidebarItem } from "./ProjectSidebarItems";
|
21
frontend/src/pages/kms/[id]/allowlist/index.tsx
Normal file
21
frontend/src/pages/kms/[id]/allowlist/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage";
|
||||
|
||||
const ProjectAllowlist = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<IPAllowlistPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectAllowlist;
|
||||
|
||||
ProjectAllowlist.requireAuth = true;
|
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage";
|
||||
|
||||
export default function ProjectIdentityDetailsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<IdentityDetailsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectIdentityDetailsPage.requireAuth = true;
|
20
frontend/src/pages/kms/[id]/members/[membershipId]/index.tsx
Normal file
20
frontend/src/pages/kms/[id]/members/[membershipId]/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage";
|
||||
|
||||
export default function Page() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MemberDetailsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Page.requireAuth = true;
|
21
frontend/src/pages/kms/[id]/members/index.tsx
Normal file
21
frontend/src/pages/kms/[id]/members/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MembersPage } from "@app/views/Project/MembersPage";
|
||||
|
||||
export default function WorkspaceMemberSettings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MembersPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
WorkspaceMemberSettings.requireAuth = true;
|
20
frontend/src/pages/kms/[id]/roles/[roleSlug]/index.tsx
Normal file
20
frontend/src/pages/kms/[id]/roles/[roleSlug]/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { RolePage } from "@app/views/Project/RolePage";
|
||||
|
||||
export default function Role() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: "Project Settings" })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<RolePage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Role.requireAuth = true;
|
22
frontend/src/pages/kms/[id]/settings/index.tsx
Normal file
22
frontend/src/pages/kms/[id]/settings/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { ProjectSettingsPage } from "@app/views/Settings/ProjectSettingsPage";
|
||||
|
||||
const ProjectSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<ProjectSettingsPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSettings;
|
||||
|
||||
ProjectSettings.requireAuth = true;
|
9
frontend/src/pages/org/[id]/cert-manager/overview.tsx
Normal file
9
frontend/src/pages/org/[id]/cert-manager/overview.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
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;
|
9
frontend/src/pages/org/[id]/kms/overview.tsx
Normal file
9
frontend/src/pages/org/[id]/kms/overview.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { ProductOverview } from "../secret-manager/overview";
|
||||
|
||||
const CmekManagerOverviewPage = () => <ProductOverview type={ProjectType.KMS} />;
|
||||
|
||||
Object.assign(CmekManagerOverviewPage, { requireAuth: true });
|
||||
|
||||
export default CmekManagerOverviewPage;
|
File diff suppressed because it is too large
Load Diff
527
frontend/src/pages/org/[id]/secret-manager/overview.tsx
Normal file
527
frontend/src/pages/org/[id]/secret-manager/overview.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowDownAZ,
|
||||
faArrowRight,
|
||||
faArrowUpZA,
|
||||
faBorderAll,
|
||||
faExclamationCircle,
|
||||
faList,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faSearch,
|
||||
faStar as faSolidStar
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { NewProjectModal } from "@app/components/v2/projects";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
enum ProjectsViewMode {
|
||||
GRID = "grid",
|
||||
LIST = "list"
|
||||
}
|
||||
|
||||
enum ProjectOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
const formatTitle = (type: ProjectType) => {
|
||||
if (type === ProjectType.SecretManager) return "Secret Management";
|
||||
if (type === ProjectType.CertificateManager) return "Cert Management";
|
||||
return "Key Management";
|
||||
};
|
||||
|
||||
const formatDescription = (type: ProjectType) => {
|
||||
if (type === ProjectType.SecretManager)
|
||||
return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
|
||||
if (type === ProjectType.CertificateManager)
|
||||
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
|
||||
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
|
||||
};
|
||||
|
||||
type Props = {
|
||||
type: ProjectType;
|
||||
};
|
||||
|
||||
// #TODO: Update all the workspaceIds
|
||||
export const ProductOverview = ({ type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { data: workspaces, isLoading: isWorkspaceLoading } = useGetUserWorkspaces({ type });
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgWorkspaces = workspaces || [];
|
||||
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
|
||||
useGetUserProjectFavorites(currentOrg?.id!);
|
||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||
|
||||
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
|
||||
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
||||
);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||
: true;
|
||||
|
||||
const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0;
|
||||
|
||||
const {
|
||||
setPage,
|
||||
perPage,
|
||||
setPerPage,
|
||||
page,
|
||||
offset,
|
||||
limit,
|
||||
toggleOrderDirection,
|
||||
orderDirection
|
||||
} = usePagination(ProjectOrderBy.Name, { initPerPage: 24 });
|
||||
|
||||
const filteredWorkspaces = useMemo(
|
||||
() =>
|
||||
orgWorkspaces
|
||||
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||
.sort((a, b) =>
|
||||
orderDirection === OrderByDirection.ASC
|
||||
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
|
||||
),
|
||||
[searchFilter, orderDirection, orgWorkspaces]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
setPage,
|
||||
offset,
|
||||
totalCount: filteredWorkspaces.length
|
||||
});
|
||||
|
||||
const { workspacesWithFaveProp } = useMemo(() => {
|
||||
const workspacesWithFav = filteredWorkspaces
|
||||
.map((w): Workspace & { isFavorite: boolean } => ({
|
||||
...w,
|
||||
isFavorite: Boolean(projectFavorites?.includes(w.id))
|
||||
}))
|
||||
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite))
|
||||
.slice(offset, limit * page);
|
||||
|
||||
return {
|
||||
workspacesWithFaveProp: workspacesWithFav
|
||||
};
|
||||
}, [filteredWorkspaces, projectFavorites]);
|
||||
|
||||
const addProjectToFavorites = async (projectId: string) => {
|
||||
try {
|
||||
if (currentOrg?.id) {
|
||||
await updateUserProjectFavorites({
|
||||
orgId: currentOrg?.id,
|
||||
projectFavorites: [...(projectFavorites || []), projectId]
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to add project to favorites.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeProjectFromFavorites = async (projectId: string) => {
|
||||
try {
|
||||
if (currentOrg?.id) {
|
||||
await updateUserProjectFavorites({
|
||||
orgId: currentOrg?.id,
|
||||
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed to remove project from favorites.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
onClick={() => {
|
||||
router.push(getProjectHomePage(workspace));
|
||||
localStorage.setItem("projectData.id", workspace.id);
|
||||
}}
|
||||
key={workspace.id}
|
||||
className="min-w-72 flex h-40 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
||||
{isFavorite ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSolidStar}
|
||||
className="text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeProjectFromFavorites(workspace.id);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faStar}
|
||||
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
addProjectToFavorites(workspace.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-1 mb-2.5 grow text-sm text-mineshaft-300"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 2
|
||||
}}
|
||||
>
|
||||
{workspace.description}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-row items-end justify-between place-self-end">
|
||||
{type === ProjectType.SecretManager && (
|
||||
<div className="mt-0 text-xs text-mineshaft-400">
|
||||
{workspace.environments?.length || 0} environments
|
||||
</div>
|
||||
)}
|
||||
<button type="button">
|
||||
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
|
||||
Explore{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
onClick={() => {
|
||||
router.push(getProjectHomePage(workspace));
|
||||
localStorage.setItem("projectData.id", workspace.id);
|
||||
}}
|
||||
key={workspace.id}
|
||||
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||
index === 0 && "rounded-t-md"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center sm:col-span-3 lg:col-span-4">
|
||||
<div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
|
||||
<div className="text-center text-sm text-mineshaft-300">
|
||||
{workspace.environments?.length || 0} environments
|
||||
</div>
|
||||
{isFavorite ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSolidStar}
|
||||
className="ml-6 text-sm text-yellow-600 hover:text-mineshaft-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeProjectFromFavorites(workspace.id);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faStar}
|
||||
className="ml-6 text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
addProjectToFavorites(workspace.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let projectsComponents: ReactNode;
|
||||
|
||||
if (filteredWorkspaces.length || isProjectViewLoading) {
|
||||
switch (projectsViewMode) {
|
||||
case ProjectsViewMode.GRID:
|
||||
projectsComponents = (
|
||||
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{isProjectViewLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="mt-0 text-lg text-mineshaft-100">
|
||||
<Skeleton className="w-3/4 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Skeleton className="w-1/2 bg-mineshaft-600" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!isProjectViewLoading && (
|
||||
<>
|
||||
{workspacesWithFaveProp.map((workspace) =>
|
||||
renderProjectGridItem(workspace, workspace.isFavorite)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
break;
|
||||
case ProjectsViewMode.LIST:
|
||||
default:
|
||||
projectsComponents = (
|
||||
<div className="mt-4 w-full rounded-md">
|
||||
{isProjectViewLoading &&
|
||||
Array.apply(0, Array(3)).map((_x, i) => (
|
||||
<div
|
||||
key={`workspace-cards-loading-${i + 1}`}
|
||||
className={`min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
|
||||
i === 0 && "rounded-t-md"
|
||||
} ${i === 2 && "rounded-b-md border-b"}`}
|
||||
>
|
||||
<Skeleton className="w-full bg-mineshaft-600" />
|
||||
</div>
|
||||
))}
|
||||
{!isProjectViewLoading &&
|
||||
workspacesWithFaveProp.map((workspace, ind) =>
|
||||
renderProjectListItem(workspace, workspace.isFavorite, ind)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else if (orgWorkspaces.length && searchFilter) {
|
||||
projectsComponents = (
|
||||
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||
<FontAwesomeIcon
|
||||
icon={faSearch}
|
||||
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||
/>
|
||||
<div className="text-center font-light">No projects match search...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
{!serverDetails?.redisConfigured && (
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||
<p className="mr-4 mb-4 font-semibold text-white">Announcements</p>
|
||||
<div className="flex w-full items-center rounded-md border border-blue-400/70 bg-blue-900/70 p-2 text-base text-mineshaft-100">
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationCircle}
|
||||
className="mr-4 p-4 text-2xl text-mineshaft-50"
|
||||
/>
|
||||
Attention: Updated versions of Infisical now require Redis for full functionality. Learn
|
||||
how to configure it
|
||||
<Link
|
||||
href="https://infisical.com/docs/self-hosting/configuration/redis"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="cursor-pointer pl-1 text-white underline underline-offset-2 duration-100 hover:text-blue-200 hover:decoration-blue-400">
|
||||
here
|
||||
</span>
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0">
|
||||
<div className="flex w-full justify-between">
|
||||
<p className="mr-4 text-3xl font-semibold text-white">{formatTitle(type)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mr-4 mt-2 text-gray-400">{formatDescription(type)}</p>
|
||||
</div>
|
||||
<div className="mt-6 flex w-full flex-row">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by project name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<Tooltip content="Toggle Sort Direction">
|
||||
<IconButton
|
||||
className="min-w-[2.4rem] border-none hover:bg-mineshaft-600"
|
||||
ariaLabel={`Sort ${
|
||||
orderDirection === OrderByDirection.ASC ? "descending" : "ascending"
|
||||
}`}
|
||||
variant="plain"
|
||||
size="xs"
|
||||
colorSchema="secondary"
|
||||
onClick={toggleOrderDirection}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={orderDirection === OrderByDirection.ASC ? faArrowDownAZ : faArrowUpZA}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="ml-2 flex rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID);
|
||||
setProjectsViewMode(ProjectsViewMode.GRID);
|
||||
}}
|
||||
ariaLabel="grid"
|
||||
size="xs"
|
||||
className={`${
|
||||
projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBorderAll} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST);
|
||||
setProjectsViewMode(ProjectsViewMode.LIST);
|
||||
}}
|
||||
ariaLabel="list"
|
||||
size="xs"
|
||||
className={`${
|
||||
projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faList} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isAddingProjectsAllowed) {
|
||||
handlePopUpOpen("addNewWs");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
className="ml-2"
|
||||
>
|
||||
Add New Project
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
{projectsComponents}
|
||||
{!isProjectViewLoading && Boolean(filteredWorkspaces.length) && (
|
||||
<Pagination
|
||||
className={
|
||||
projectsViewMode === ProjectsViewMode.GRID
|
||||
? "col-span-full !justify-start border-transparent bg-transparent pl-2"
|
||||
: "rounded-b-md border border-mineshaft-600"
|
||||
}
|
||||
perPage={perPage}
|
||||
perPageList={[12, 24, 48, 96]}
|
||||
count={filteredWorkspaces.length}
|
||||
page={page}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
/>
|
||||
)}
|
||||
{isWorkspaceEmpty && (
|
||||
<div className="mt-4 w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
|
||||
<FontAwesomeIcon
|
||||
icon={faFolderOpen}
|
||||
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
|
||||
/>
|
||||
<div className="text-center font-light">
|
||||
You are not part of any projects in this organization yet. When you are, they will
|
||||
appear here.
|
||||
</div>
|
||||
<div className="mt-0.5 text-center font-light">
|
||||
Create a new project, or ask other organization members to give you necessary
|
||||
permissions.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
projectType={type}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You have exceeded the number of projects allowed on the free plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SecretManagerOverviewPage = () => <ProductOverview type={ProjectType.SecretManager} />;
|
||||
|
||||
Object.assign(SecretManagerOverviewPage, { requireAuth: true });
|
||||
|
||||
export default SecretManagerOverviewPage;
|
21
frontend/src/pages/secret-manager/[id]/allowlist/index.tsx
Normal file
21
frontend/src/pages/secret-manager/[id]/allowlist/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { IPAllowlistPage } from "@app/views/Project/IPAllowListPage";
|
||||
|
||||
const ProjectAllowlist = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<IPAllowlistPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectAllowlist;
|
||||
|
||||
ProjectAllowlist.requireAuth = true;
|
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage";
|
||||
|
||||
export default function ProjectIdentityDetailsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<IdentityDetailsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectIdentityDetailsPage.requireAuth = true;
|
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage";
|
||||
|
||||
export default function Page() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MemberDetailsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Page.requireAuth = true;
|
21
frontend/src/pages/secret-manager/[id]/members/index.tsx
Normal file
21
frontend/src/pages/secret-manager/[id]/members/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MembersPage } from "@app/views/Project/MembersPage";
|
||||
|
||||
export default function WorkspaceMemberSettings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MembersPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
WorkspaceMemberSettings.requireAuth = true;
|
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { RolePage } from "@app/views/Project/RolePage";
|
||||
|
||||
export default function Role() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: "Project Settings" })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<RolePage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Role.requireAuth = true;
|
22
frontend/src/pages/secret-manager/[id]/settings/index.tsx
Normal file
22
frontend/src/pages/secret-manager/[id]/settings/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { ProjectSettingsPage } from "@app/views/Settings/ProjectSettingsPage";
|
||||
|
||||
const ProjectSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<ProjectSettingsPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSettings;
|
||||
|
||||
ProjectSettings.requireAuth = true;
|
@@ -16,6 +16,7 @@ import { useServerConfig } from "@app/context";
|
||||
import { useVerifySignupEmailVerificationCode } from "@app/hooks/api";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
/**
|
||||
* @returns the signup page
|
||||
@@ -47,7 +48,7 @@ export default function SignUp() {
|
||||
const tryAuth = async () => {
|
||||
try {
|
||||
const userOrgs = await fetchOrganizations();
|
||||
router.push(`/org/${userOrgs[0].id}/overview`);
|
||||
router.push(`/org/${userOrgs[0].id}/${ProjectType.SecretManager}/overview`);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
@@ -90,7 +91,7 @@ export default function SignUp() {
|
||||
|
||||
if (!serverDetails?.emailConfigured && step === 5) {
|
||||
const userOrgs = await fetchOrganizations();
|
||||
router.push(`/org/${userOrgs[0].id}/overview`);
|
||||
router.push(`/org/${userOrgs[0].id}/${ProjectType.SecretManager}/overview`);
|
||||
}
|
||||
})();
|
||||
}, [step]);
|
||||
|
@@ -31,6 +31,7 @@ import {
|
||||
} from "@app/hooks/api/auth/queries";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
|
||||
import { Mfa } from "@app/views/Login/Mfa";
|
||||
|
||||
@@ -386,7 +387,7 @@ export default function SignupInvite() {
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
router.push(`/org/${organizationId}/overview`);
|
||||
router.push(`/org/${organizationId}/${ProjectType.SecretManager}/overview`);
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user