mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 11:27:32 +00:00
improvement: address requested feedback
This commit is contained in:
@@ -13,7 +13,6 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.jsonb("environments").notNullable();
|
t.jsonb("environments").notNullable();
|
||||||
t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
t.unique(["orgId", "name"]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.ProjectTemplates);
|
await createOnUpdateTrigger(knex, TableName.ProjectTemplates);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const ProjectTemplatesSchema = z.object({
|
|||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
roles: z.unknown(),
|
roles: z.unknown(),
|
||||||
environments: z.unknown(),
|
environments: z.unknown(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import { z } from "zod";
|
|||||||
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import {
|
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||||
DefaultProjectTemplateIdentifier,
|
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||||
ProjectTemplateDefaultEnvironments
|
|
||||||
} from "@app/ee/services/project-template/project-template-constants";
|
|
||||||
import { ProjectTemplates } from "@app/lib/api-docs";
|
import { ProjectTemplates } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 1_048_576; // 1MB
|
||||||
|
|
||||||
const SlugSchema = z
|
const SlugSchema = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -57,6 +57,9 @@ const ProjectTemplateRolesSchema = z
|
|||||||
.superRefine((roles, ctx) => {
|
.superRefine((roles, ctx) => {
|
||||||
if (!roles.length) return;
|
if (!roles.length) return;
|
||||||
|
|
||||||
|
if (Buffer.byteLength(JSON.stringify(roles)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
||||||
|
|
||||||
if (new Set(roles.map((v) => v.slug)).size !== roles.length)
|
if (new Set(roles.map((v) => v.slug)).size !== roles.length)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" });
|
||||||
|
|
||||||
@@ -81,6 +84,9 @@ const ProjectTemplateEnvironmentsSchema = z
|
|||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1)
|
||||||
.superRefine((environments, ctx) => {
|
.superRefine((environments, ctx) => {
|
||||||
|
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
||||||
|
|
||||||
if (new Set(environments.map((v) => v.name)).size !== environments.length)
|
if (new Set(environments.map((v) => v.name)).size !== environments.length)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" });
|
||||||
|
|
||||||
@@ -116,7 +122,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
|
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
|
||||||
|
|
||||||
const auditTemplates = projectTemplates.filter((template) => template.name !== DefaultProjectTemplateIdentifier);
|
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -153,7 +159,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const projectTemplate = await server.services.projectTemplate.findProjectTemplatesById(
|
const projectTemplate = await server.services.projectTemplate.findProjectTemplateById(
|
||||||
req.params.templateId,
|
req.params.templateId,
|
||||||
req.permission
|
req.permission
|
||||||
);
|
);
|
||||||
@@ -182,10 +188,10 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
schema: {
|
schema: {
|
||||||
description: "Create a project template.",
|
description: "Create a project template.",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: SlugSchema.refine((val) => val !== DefaultProjectTemplateIdentifier, {
|
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||||
message: `The project template name "${DefaultProjectTemplateIdentifier}" is reserved.`
|
message: `The requested project template name is reserved.`
|
||||||
}).describe(ProjectTemplates.CREATE.name),
|
}).describe(ProjectTemplates.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(ProjectTemplates.CREATE.description),
|
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
||||||
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
||||||
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
|
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
|
||||||
ProjectTemplates.CREATE.environments
|
ProjectTemplates.CREATE.environments
|
||||||
@@ -224,12 +230,12 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
description: "Update a project template.",
|
description: "Update a project template.",
|
||||||
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
|
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: SlugSchema.refine((val) => val !== DefaultProjectTemplateIdentifier, {
|
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||||
message: `The project template name "${DefaultProjectTemplateIdentifier}" is reserved.`
|
message: `The requested project template name is reserved.`
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(ProjectTemplates.UPDATE.name),
|
.describe(ProjectTemplates.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(ProjectTemplates.UPDATE.description),
|
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
|
||||||
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
|
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
|
||||||
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
|
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1663,7 +1663,7 @@ interface DeleteProjectTemplateEvent {
|
|||||||
interface ApplyProjectTemplateEvent {
|
interface ApplyProjectTemplateEvent {
|
||||||
type: EventType.APPLY_PROJECT_TEMPLATE;
|
type: EventType.APPLY_PROJECT_TEMPLATE;
|
||||||
metadata: {
|
metadata: {
|
||||||
templateId: string;
|
template: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
},
|
},
|
||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false
|
projectTemplates: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
|||||||
@@ -1,25 +1,5 @@
|
|||||||
import { TUnpackedPermission } from "@app/ee/services/project-template/project-template-types";
|
|
||||||
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
|
|
||||||
|
|
||||||
export const ProjectTemplateDefaultEnvironments = [
|
export const ProjectTemplateDefaultEnvironments = [
|
||||||
{ name: "Development", slug: "dev", position: 1 },
|
{ name: "Development", slug: "dev", position: 1 },
|
||||||
{ name: "Staging", slug: "staging", position: 2 },
|
{ name: "Staging", slug: "staging", position: 2 },
|
||||||
{ name: "Production", slug: "prod", position: 3 }
|
{ name: "Production", slug: "prod", position: 3 }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DefaultProjectTemplateIdentifier = "default";
|
|
||||||
|
|
||||||
export const getDefaultProjectTemplate = (orgId: string) => ({
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
|
|
||||||
name: DefaultProjectTemplateIdentifier,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
description: "Infisical's default project template",
|
|
||||||
environments: ProjectTemplateDefaultEnvironments,
|
|
||||||
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
|
|
||||||
name,
|
|
||||||
slug,
|
|
||||||
permissions: permissions as TUnpackedPermission[]
|
|
||||||
})),
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||||
|
import {
|
||||||
|
InfisicalProjectTemplate,
|
||||||
|
TUnpackedPermission
|
||||||
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
|
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
|
||||||
|
|
||||||
|
export const getDefaultProjectTemplate = (orgId: string) => ({
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
|
||||||
|
name: InfisicalProjectTemplate.Default,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
description: "Infisical's default project template",
|
||||||
|
environments: ProjectTemplateDefaultEnvironments,
|
||||||
|
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
permissions: permissions as TUnpackedPermission[]
|
||||||
|
})),
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
export const isInfisicalProjectTemplate = (template: string) =>
|
||||||
|
Object.values(InfisicalProjectTemplate).includes(template as InfisicalProjectTemplate);
|
||||||
@@ -5,7 +5,7 @@ import { TProjectTemplates } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-constants";
|
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||||
import {
|
import {
|
||||||
TCreateProjectTemplateDTO,
|
TCreateProjectTemplateDTO,
|
||||||
TProjectTemplateEnvironment,
|
TProjectTemplateEnvironment,
|
||||||
@@ -50,6 +50,13 @@ export const projectTemplateServiceFactory = ({
|
|||||||
projectTemplateDAL
|
projectTemplateDAL
|
||||||
}: TProjectTemplatesServiceFactoryDep) => {
|
}: TProjectTemplatesServiceFactoryDep) => {
|
||||||
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
|
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor.type,
|
actor.type,
|
||||||
actor.id,
|
actor.id,
|
||||||
@@ -60,13 +67,6 @@ export const projectTemplateServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
|
||||||
|
|
||||||
if (!plan.projectTemplates)
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectTemplates = await projectTemplateDAL.find({
|
const projectTemplates = await projectTemplateDAL.find({
|
||||||
orgId: actor.orgId
|
orgId: actor.orgId
|
||||||
});
|
});
|
||||||
@@ -77,17 +77,7 @@ export const projectTemplateServiceFactory = ({
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const findProjectTemplatesById = async (id: string, actor: OrgServiceActor) => {
|
const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
|
||||||
actor.type,
|
|
||||||
actor.id,
|
|
||||||
actor.orgId,
|
|
||||||
actor.authMethod,
|
|
||||||
actor.orgId
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
if (!plan.projectTemplates)
|
if (!plan.projectTemplates)
|
||||||
@@ -95,13 +85,48 @@ export const projectTemplateServiceFactory = ({
|
|||||||
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
|
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
});
|
});
|
||||||
|
|
||||||
const [projectTemplate] = await projectTemplateDAL.find({
|
const projectTemplate = await projectTemplateDAL.findOne({ name, orgId: actor.orgId });
|
||||||
id,
|
|
||||||
orgId: actor.orgId // ensure from user org
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` });
|
||||||
});
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...$unpackProjectTemplate(projectTemplate),
|
||||||
|
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const findProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to access project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectTemplate = await projectTemplateDAL.findById(id);
|
||||||
|
|
||||||
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...$unpackProjectTemplate(projectTemplate),
|
...$unpackProjectTemplate(projectTemplate),
|
||||||
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
|
packedRoles: projectTemplate.roles as TProjectTemplateRole[] // preserve packed for when applying template
|
||||||
@@ -112,6 +137,13 @@ export const projectTemplateServiceFactory = ({
|
|||||||
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
|
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
|
if (!plan.projectTemplates)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
|
});
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor.type,
|
actor.type,
|
||||||
actor.id,
|
actor.id,
|
||||||
@@ -122,11 +154,16 @@ export const projectTemplateServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
const isConflictingName = Boolean(
|
||||||
|
await projectTemplateDAL.findOne({
|
||||||
|
name: params.name,
|
||||||
|
orgId: actor.orgId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!plan.projectTemplates)
|
if (isConflictingName)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
|
message: `A project template with the name "${params.name}" already exists.`
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectTemplate = await projectTemplateDAL.create({
|
const projectTemplate = await projectTemplateDAL.create({
|
||||||
@@ -144,16 +181,6 @@ export const projectTemplateServiceFactory = ({
|
|||||||
{ roles, environments, ...params }: TUpdateProjectTemplateDTO,
|
{ roles, environments, ...params }: TUpdateProjectTemplateDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
|
||||||
actor.type,
|
|
||||||
actor.id,
|
|
||||||
actor.orgId,
|
|
||||||
actor.authMethod,
|
|
||||||
actor.orgId
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
if (!plan.projectTemplates)
|
if (!plan.projectTemplates)
|
||||||
@@ -161,7 +188,35 @@ export const projectTemplateServiceFactory = ({
|
|||||||
message: "Failed to update project template due to plan restriction. Upgrade plan to access project templates."
|
message: "Failed to update project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectTemplate = await projectTemplateDAL.updateById(id, {
|
const projectTemplate = await projectTemplateDAL.findById(id);
|
||||||
|
|
||||||
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
if (params.name && projectTemplate.name !== params.name) {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
await projectTemplateDAL.findOne({
|
||||||
|
name: params.name,
|
||||||
|
orgId: projectTemplate.orgId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A project template with the name "${params.name}" already exists.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProjectTemplate = await projectTemplateDAL.updateById(id, {
|
||||||
...params,
|
...params,
|
||||||
roles: roles
|
roles: roles
|
||||||
? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) })))
|
? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) })))
|
||||||
@@ -169,20 +224,10 @@ export const projectTemplateServiceFactory = ({
|
|||||||
environments: environments ? JSON.stringify(environments) : undefined
|
environments: environments ? JSON.stringify(environments) : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
return $unpackProjectTemplate(projectTemplate);
|
return $unpackProjectTemplate(updatedProjectTemplate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
|
const deleteProjectTemplateById = async (id: string, actor: OrgServiceActor) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
|
||||||
actor.type,
|
|
||||||
actor.id,
|
|
||||||
actor.orgId,
|
|
||||||
actor.authMethod,
|
|
||||||
actor.orgId
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
if (!plan.projectTemplates)
|
if (!plan.projectTemplates)
|
||||||
@@ -190,9 +235,23 @@ export const projectTemplateServiceFactory = ({
|
|||||||
message: "Failed to delete project template due to plan restriction. Upgrade plan to access project templates."
|
message: "Failed to delete project template due to plan restriction. Upgrade plan to access project templates."
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectTemplate = await projectTemplateDAL.deleteById(id);
|
const projectTemplate = await projectTemplateDAL.findById(id);
|
||||||
|
|
||||||
return $unpackProjectTemplate(projectTemplate);
|
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectTemplate.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
|
const deletedProjectTemplate = await projectTemplateDAL.deleteById(id);
|
||||||
|
|
||||||
|
return $unpackProjectTemplate(deletedProjectTemplate);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -200,6 +259,7 @@ export const projectTemplateServiceFactory = ({
|
|||||||
createProjectTemplate,
|
createProjectTemplate,
|
||||||
updateProjectTemplateById,
|
updateProjectTemplateById,
|
||||||
deleteProjectTemplateById,
|
deleteProjectTemplateById,
|
||||||
findProjectTemplatesById
|
findProjectTemplateById,
|
||||||
|
findProjectTemplateByName
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,3 +22,7 @@ export type TCreateProjectTemplateDTO = {
|
|||||||
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
|
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
|
||||||
|
|
||||||
export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>;
|
export type TUnpackedPermission = z.infer<typeof UnpackedPermissionSchema>;
|
||||||
|
|
||||||
|
export enum InfisicalProjectTemplate {
|
||||||
|
Default = "default"
|
||||||
|
}
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ export const PROJECTS = {
|
|||||||
organizationSlug: "The slug of the organization to create the project in.",
|
organizationSlug: "The slug of the organization to create the project in.",
|
||||||
projectName: "The name of the project to create.",
|
projectName: "The name of the project to create.",
|
||||||
slug: "An optional slug for the project.",
|
slug: "An optional slug for the project.",
|
||||||
templateId: "The ID of the project template, if specified, to apply to this project."
|
template: "The name of the project template, if specified, to apply to this project."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
workspaceId: "The ID of the project to delete."
|
workspaceId: "The ID of the project to delete."
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ProjectKeysSchema
|
ProjectKeysSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
@@ -170,7 +171,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.CREATE.slug),
|
.describe(PROJECTS.CREATE.slug),
|
||||||
kmsKeyId: z.string().optional(),
|
kmsKeyId: z.string().optional(),
|
||||||
templateId: z.string().uuid().optional().describe(PROJECTS.CREATE.templateId)
|
template: z
|
||||||
|
.string()
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Template name must be in slug format"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default(InfisicalProjectTemplate.Default)
|
||||||
|
.describe(PROJECTS.CREATE.template)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -188,7 +196,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
workspaceName: req.body.projectName,
|
workspaceName: req.body.projectName,
|
||||||
slug: req.body.slug,
|
slug: req.body.slug,
|
||||||
kmsKeyId: req.body.kmsKeyId,
|
kmsKeyId: req.body.kmsKeyId,
|
||||||
templateId: req.body.templateId
|
template: req.body.template
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
@@ -201,14 +209,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.body.templateId) {
|
if (req.body.template) {
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.APPLY_PROJECT_TEMPLATE,
|
type: EventType.APPLY_PROJECT_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
templateId: req.body.templateId,
|
template: req.body.template,
|
||||||
projectId: project.id
|
projectId: project.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
@@ -152,7 +153,7 @@ export const projectServiceFactory = ({
|
|||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
tx: trx,
|
tx: trx,
|
||||||
createDefaultEnvs = true,
|
createDefaultEnvs = true,
|
||||||
templateId
|
template = InfisicalProjectTemplate.Default
|
||||||
}: TCreateProjectDTO) => {
|
}: TCreateProjectDTO) => {
|
||||||
const organization = await orgDAL.findOne({ id: actorOrgId });
|
const organization = await orgDAL.findOne({ id: actorOrgId });
|
||||||
|
|
||||||
@@ -187,23 +188,19 @@ export const projectServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let projectTemplate: Awaited<ReturnType<typeof projectTemplateService.findProjectTemplatesById>> | null = null;
|
let projectTemplate: Awaited<ReturnType<typeof projectTemplateService.findProjectTemplateByName>> | null = null;
|
||||||
|
|
||||||
if (templateId) {
|
switch (template) {
|
||||||
if (!plan.projectTemplates)
|
case InfisicalProjectTemplate.Default:
|
||||||
throw new BadRequestError({
|
projectTemplate = null;
|
||||||
message:
|
break;
|
||||||
"Failed to apply project template due to plan restriction. Upgrade plan to access project templates."
|
default:
|
||||||
|
projectTemplate = await projectTemplateService.findProjectTemplateByName(template, {
|
||||||
|
id: actorId,
|
||||||
|
orgId: organization.id,
|
||||||
|
type: actor,
|
||||||
|
authMethod: actorAuthMethod
|
||||||
});
|
});
|
||||||
|
|
||||||
projectTemplate = await projectTemplateService.findProjectTemplatesById(templateId, {
|
|
||||||
id: actorId,
|
|
||||||
orgId: organization.id,
|
|
||||||
type: actor,
|
|
||||||
authMethod: actorAuthMethod
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!projectTemplate) throw new NotFoundError({ message: `Project template with ID ${templateId} not found.` });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await projectDAL.create(
|
const project = await projectDAL.create(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export type TCreateProjectDTO = {
|
|||||||
slug?: string;
|
slug?: string;
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
createDefaultEnvs?: boolean;
|
createDefaultEnvs?: boolean;
|
||||||
templateId?: string;
|
template?: string;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ In the following steps, we'll explore how to use a project template when creatin
|
|||||||
Your project will be provisioned with the configured template roles and environments.
|
Your project will be provisioned with the configured template roles and environments.
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="API">
|
<Tab title="API">
|
||||||
To use a project template, make an API request to the [Create Project](/api-reference/endpoints/workspaces/create-workspace) API endpoint with the specified template ID included.
|
To use a project template, make an API request to the [Create Project](/api-reference/endpoints/workspaces/create-workspace) API endpoint with the specified template name included.
|
||||||
|
|
||||||
### Sample request
|
### Sample request
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ In the following steps, we'll explore how to use a project template when creatin
|
|||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
--data '{
|
--data '{
|
||||||
"projectName": "My Project",
|
"projectName": "My Project",
|
||||||
"templateId": "<template-id>",
|
"template": "<template-name>", // defaults to "default"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ export type TDeleteProjectTemplateDTO = {
|
|||||||
templateId: string;
|
templateId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultProjectTemplateIdentifier = "default";
|
export enum InfisicalProjectTemplate {
|
||||||
|
Default = "default"
|
||||||
|
}
|
||||||
|
|||||||
@@ -209,20 +209,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
|||||||
export const createWorkspace = ({
|
export const createWorkspace = ({
|
||||||
projectName,
|
projectName,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
templateId
|
template
|
||||||
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
||||||
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, templateId });
|
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, template });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||||
mutationFn: async ({ projectName, kmsKeyId, templateId }) =>
|
mutationFn: async ({ projectName, kmsKeyId, template }) =>
|
||||||
createWorkspace({
|
createWorkspace({
|
||||||
projectName,
|
projectName,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
templateId
|
template
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export type TGetUpgradeProjectStatusDTO = {
|
|||||||
export type CreateWorkspaceDTO = {
|
export type CreateWorkspaceDTO = {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
templateId?: string;
|
template?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||||
|
|||||||
@@ -79,10 +79,7 @@ import {
|
|||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||||
import {
|
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||||
DefaultProjectTemplateIdentifier,
|
|
||||||
useListProjectTemplates
|
|
||||||
} from "@app/hooks/api/projectTemplates";
|
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||||
@@ -130,7 +127,7 @@ const formSchema = yup.object({
|
|||||||
.max(64, "Too long, maximum length is 64 characters"),
|
.max(64, "Too long, maximum length is 64 characters"),
|
||||||
addMembers: yup.bool().required().label("Add Members"),
|
addMembers: yup.bool().required().label("Add Members"),
|
||||||
kmsKeyId: yup.string().label("KMS Key ID"),
|
kmsKeyId: yup.string().label("KMS Key ID"),
|
||||||
templateId: yup.string().label("Project Template ID")
|
template: yup.string().label("Project Template Name")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||||
@@ -288,12 +285,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
enabled: canReadProjectTemplates && subscription?.projectTemplates
|
enabled: canReadProjectTemplates && subscription?.projectTemplates
|
||||||
});
|
});
|
||||||
|
|
||||||
const onCreateProject = async ({
|
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
|
||||||
name,
|
|
||||||
addMembers,
|
|
||||||
kmsKeyId,
|
|
||||||
templateId
|
|
||||||
}: TAddProjectFormData) => {
|
|
||||||
// type check
|
// type check
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -305,7 +297,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
} = await createWs.mutateAsync({
|
} = await createWs.mutateAsync({
|
||||||
projectName: name,
|
projectName: name,
|
||||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||||
templateId: templateId === DefaultProjectTemplateIdentifier ? undefined : templateId
|
template
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addMembers) {
|
if (addMembers) {
|
||||||
@@ -948,7 +940,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="templateId"
|
name="template"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Read}
|
I={OrgPermissionActions.Read}
|
||||||
@@ -971,25 +963,24 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={DefaultProjectTemplateIdentifier}
|
defaultValue={InfisicalProjectTemplate.Default}
|
||||||
placeholder={DefaultProjectTemplateIdentifier}
|
placeholder={InfisicalProjectTemplate.Default}
|
||||||
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
className="w-44"
|
className="w-44"
|
||||||
>
|
>
|
||||||
<SelectItem value={DefaultProjectTemplateIdentifier}>
|
{projectTemplates.length
|
||||||
{DefaultProjectTemplateIdentifier}
|
? projectTemplates.map((template) => (
|
||||||
</SelectItem>
|
<SelectItem key={template.id} value={template.name}>
|
||||||
{projectTemplates
|
{template.name}
|
||||||
.filter(
|
</SelectItem>
|
||||||
(template) => template.name !== DefaultProjectTemplateIdentifier
|
))
|
||||||
)
|
: Object.values(InfisicalProjectTemplate).map((template) => (
|
||||||
.map((template) => (
|
<SelectItem key={template} value={template}>
|
||||||
<SelectItem key={template.id} value={template.id}>
|
{template}
|
||||||
{template.name}
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ import {
|
|||||||
useRegisterUserAction
|
useRegisterUserAction
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||||
import {
|
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||||
DefaultProjectTemplateIdentifier,
|
|
||||||
useListProjectTemplates
|
|
||||||
} from "@app/hooks/api/projectTemplates";
|
|
||||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
@@ -488,7 +485,7 @@ const formSchema = yup.object({
|
|||||||
.max(64, "Too long, maximum length is 64 characters"),
|
.max(64, "Too long, maximum length is 64 characters"),
|
||||||
addMembers: yup.bool().required().label("Add Members"),
|
addMembers: yup.bool().required().label("Add Members"),
|
||||||
kmsKeyId: yup.string().label("KMS Key ID"),
|
kmsKeyId: yup.string().label("KMS Key ID"),
|
||||||
templateId: yup.string().label("Project Template ID")
|
template: yup.string().label("Project Template Name")
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||||
@@ -543,12 +540,7 @@ const OrganizationPage = () => {
|
|||||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||||
});
|
});
|
||||||
|
|
||||||
const onCreateProject = async ({
|
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
|
||||||
name,
|
|
||||||
addMembers,
|
|
||||||
kmsKeyId,
|
|
||||||
templateId
|
|
||||||
}: TAddProjectFormData) => {
|
|
||||||
// type check
|
// type check
|
||||||
if (!currentOrg) return;
|
if (!currentOrg) return;
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -560,7 +552,7 @@ const OrganizationPage = () => {
|
|||||||
} = await createWs.mutateAsync({
|
} = await createWs.mutateAsync({
|
||||||
projectName: name,
|
projectName: name,
|
||||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||||
templateId: templateId === DefaultProjectTemplateIdentifier ? undefined : templateId
|
template
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addMembers) {
|
if (addMembers) {
|
||||||
@@ -1076,7 +1068,7 @@ const OrganizationPage = () => {
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="templateId"
|
name="template"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Read}
|
I={OrgPermissionActions.Read}
|
||||||
@@ -1099,25 +1091,24 @@ const OrganizationPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={DefaultProjectTemplateIdentifier}
|
defaultValue={InfisicalProjectTemplate.Default}
|
||||||
placeholder={DefaultProjectTemplateIdentifier}
|
placeholder={InfisicalProjectTemplate.Default}
|
||||||
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
className="w-44"
|
className="w-44"
|
||||||
>
|
>
|
||||||
<SelectItem value={DefaultProjectTemplateIdentifier}>
|
{projectTemplates.length
|
||||||
{DefaultProjectTemplateIdentifier}
|
? projectTemplates.map((template) => (
|
||||||
</SelectItem>
|
<SelectItem key={template.id} value={template.name}>
|
||||||
{projectTemplates
|
{template.name}
|
||||||
.filter(
|
</SelectItem>
|
||||||
(template) => template.name !== DefaultProjectTemplateIdentifier
|
))
|
||||||
)
|
: Object.values(InfisicalProjectTemplate).map((template) => (
|
||||||
.map((template) => (
|
<SelectItem key={template} value={template}>
|
||||||
<SelectItem key={template.id} value={template.id}>
|
{template}
|
||||||
{template.name}
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+6
-4
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { Button, EmptyState, Spinner } from "@app/components/v2";
|
import { Button, EmptyState, Spinner } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
DefaultProjectTemplateIdentifier,
|
InfisicalProjectTemplate,
|
||||||
TProjectTemplate,
|
TProjectTemplate,
|
||||||
useGetProjectTemplateById
|
useGetProjectTemplateById
|
||||||
} from "@app/hooks/api/projectTemplates";
|
} from "@app/hooks/api/projectTemplates";
|
||||||
@@ -16,11 +16,13 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
|
export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
|
||||||
const isDefault = template.name === DefaultProjectTemplateIdentifier;
|
const isInfisicalTemplate = Object.values(InfisicalProjectTemplate).includes(
|
||||||
|
template.name as InfisicalProjectTemplate
|
||||||
|
);
|
||||||
|
|
||||||
const { data: projectTemplate, isLoading } = useGetProjectTemplateById(template.id, {
|
const { data: projectTemplate, isLoading } = useGetProjectTemplateById(template.id, {
|
||||||
initialData: template,
|
initialData: template,
|
||||||
enabled: !isDefault
|
enabled: !isInfisicalTemplate
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,7 +43,7 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : projectTemplate ? (
|
) : projectTemplate ? (
|
||||||
<EditProjectTemplate
|
<EditProjectTemplate
|
||||||
isDefault={isDefault}
|
isInfisicalTemplate={isInfisicalTemplate}
|
||||||
projectTemplate={projectTemplate}
|
projectTemplate={projectTemplate}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+8
-5
@@ -23,10 +23,10 @@ import { ProjectTemplateRolesSection } from "./ProjectTemplateRolesSection";
|
|||||||
type Props = {
|
type Props = {
|
||||||
projectTemplate: TProjectTemplate;
|
projectTemplate: TProjectTemplate;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
isDefault: boolean;
|
isInfisicalTemplate: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditProjectTemplate = ({ isDefault, projectTemplate, onBack }: Props) => {
|
export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBack }: Props) => {
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"removeTemplate",
|
"removeTemplate",
|
||||||
"editDetails"
|
"editDetails"
|
||||||
@@ -63,7 +63,7 @@ export const EditProjectTemplate = ({ isDefault, projectTemplate, onBack }: Prop
|
|||||||
<h3 className="text-xl font-semibold">{name}</h3>
|
<h3 className="text-xl font-semibold">{name}</h3>
|
||||||
<h2 className="text-sm text-mineshaft-400">{description || "Project Template"}</h2>
|
<h2 className="text-sm text-mineshaft-400">{description || "Project Template"}</h2>
|
||||||
</div>
|
</div>
|
||||||
{!isDefault && (
|
{!isInfisicalTemplate && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -117,9 +117,12 @@ export const EditProjectTemplate = ({ isDefault, projectTemplate, onBack }: Prop
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ProjectTemplateEnvironmentsForm isDefault={isDefault} projectTemplate={projectTemplate} />
|
<ProjectTemplateEnvironmentsForm
|
||||||
|
isInfisicalTemplate={isInfisicalTemplate}
|
||||||
|
projectTemplate={projectTemplate}
|
||||||
|
/>
|
||||||
<ProjectTemplateRolesSection
|
<ProjectTemplateRolesSection
|
||||||
isDefaultTemplate={isDefault}
|
isInfisicalTemplate={isInfisicalTemplate}
|
||||||
projectTemplate={projectTemplate}
|
projectTemplate={projectTemplate}
|
||||||
/>
|
/>
|
||||||
<ProjectTemplateDetailsModal
|
<ProjectTemplateDetailsModal
|
||||||
|
|||||||
+1
-1
@@ -169,7 +169,7 @@ export const ProjectTemplateEditRoleForm = ({
|
|||||||
label="Name"
|
label="Name"
|
||||||
className="mb-0 flex-1"
|
className="mb-0 flex-1"
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="Role name..." />
|
<Input {...field} autoFocus placeholder="Role name..." />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+11
-8
@@ -25,7 +25,7 @@ import { slugSchema } from "@app/lib/schemas";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectTemplate: TProjectTemplate;
|
projectTemplate: TProjectTemplate;
|
||||||
isDefault: boolean;
|
isInfisicalTemplate: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -39,7 +39,10 @@ const formSchema = z.object({
|
|||||||
|
|
||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }: Props) => {
|
export const ProjectTemplateEnvironmentsForm = ({
|
||||||
|
projectTemplate,
|
||||||
|
isInfisicalTemplate
|
||||||
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -94,13 +97,13 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
|
|||||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">Project Environments</h2>
|
<h2 className="text-lg font-semibold">Project Environments</h2>
|
||||||
{!isDefault && (
|
{!isInfisicalTemplate && (
|
||||||
<p className="text-sm text-mineshaft-400">
|
<p className="text-sm text-mineshaft-400">
|
||||||
Add, rename, remove and reorder environments for this project template
|
Add, rename, remove and reorder environments for this project template
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isDefault && (
|
{!isInfisicalTemplate && (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.ProjectTemplates}
|
a={OrgPermissionSubjects.ProjectTemplates}
|
||||||
@@ -129,7 +132,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Friendly Name</Th>
|
<Th>Friendly Name</Th>
|
||||||
<Th>Slug</Th>
|
<Th>Slug</Th>
|
||||||
{!isDefault && (
|
{!isInfisicalTemplate && (
|
||||||
<Th>
|
<Th>
|
||||||
<div className="flex w-full justify-end normal-case">
|
<div className="flex w-full justify-end normal-case">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
@@ -161,7 +164,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
|
|||||||
{environments.map(({ id, name, slug }, pos) => (
|
{environments.map(({ id, name, slug }, pos) => (
|
||||||
<Tr key={id}>
|
<Tr key={id}>
|
||||||
<Td>
|
<Td>
|
||||||
{isDefault ? (
|
{isInfisicalTemplate ? (
|
||||||
name
|
name
|
||||||
) : (
|
) : (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
@@ -187,7 +190,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
|
|||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{isDefault ? (
|
{isInfisicalTemplate ? (
|
||||||
slug
|
slug
|
||||||
) : (
|
) : (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
@@ -212,7 +215,7 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:
|
|||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
{!isDefault && (
|
{!isInfisicalTemplate && (
|
||||||
<Td className="flex items-center justify-end">
|
<Td className="flex items-center justify-end">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
|
|||||||
+4
-4
@@ -27,10 +27,10 @@ import { ProjectTemplateEditRoleForm } from "./ProjectTemplateEditRoleForm";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectTemplate: TProjectTemplate;
|
projectTemplate: TProjectTemplate;
|
||||||
isDefaultTemplate: boolean;
|
isInfisicalTemplate: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectTemplateRolesSection = ({ projectTemplate, isDefaultTemplate }: Props) => {
|
export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTemplate }: Props) => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
"removeRole",
|
"removeRole",
|
||||||
"editRole"
|
"editRole"
|
||||||
@@ -108,12 +108,12 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isDefaultTemplate
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">Project Roles</h2>
|
<h2 className="text-lg font-semibold">Project Roles</h2>
|
||||||
<p className="text-sm text-mineshaft-400">
|
<p className="text-sm text-mineshaft-400">
|
||||||
{isDefaultTemplate
|
{isInfisicalTemplate
|
||||||
? "Click a role to view the associated permissions"
|
? "Click a role to view the associated permissions"
|
||||||
: "Add, edit and remove roles for this project template"}
|
: "Add, edit and remove roles for this project template"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!isDefaultTemplate && (
|
{!isInfisicalTemplate && (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.ProjectTemplates}
|
a={OrgPermissionSubjects.ProjectTemplates}
|
||||||
|
|||||||
Reference in New Issue
Block a user