diff --git a/backend/src/db/migrations/20241101174939_project-templates.ts b/backend/src/db/migrations/20241101174939_project-templates.ts index c75978ce3..ba78adb47 100644 --- a/backend/src/db/migrations/20241101174939_project-templates.ts +++ b/backend/src/db/migrations/20241101174939_project-templates.ts @@ -13,7 +13,6 @@ export async function up(knex: Knex): Promise { t.jsonb("environments").notNullable(); t.uuid("orgId").notNullable().references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.timestamps(true, true, true); - t.unique(["orgId", "name"]); }); await createOnUpdateTrigger(knex, TableName.ProjectTemplates); diff --git a/backend/src/db/schemas/project-templates.ts b/backend/src/db/schemas/project-templates.ts index b3645aa32..68f37d256 100644 --- a/backend/src/db/schemas/project-templates.ts +++ b/backend/src/db/schemas/project-templates.ts @@ -13,7 +13,7 @@ export const ProjectTemplatesSchema = z.object({ description: z.string().nullable().optional(), roles: z.unknown(), environments: z.unknown(), - orgId: z.string().uuid().nullable().optional(), + orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/ee/routes/v1/project-template-router.ts b/backend/src/ee/routes/v1/project-template-router.ts index 17a2b85a4..611eab7ab 100644 --- a/backend/src/ee/routes/v1/project-template-router.ts +++ b/backend/src/ee/routes/v1/project-template-router.ts @@ -4,16 +4,16 @@ import { z } from "zod"; import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; -import { - DefaultProjectTemplateIdentifier, - ProjectTemplateDefaultEnvironments -} from "@app/ee/services/project-template/project-template-constants"; +import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants"; +import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { ProjectTemplates } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { AuthMode } from "@app/services/auth/auth-type"; +const MAX_JSON_SIZE_LIMIT_IN_BYTES = 1_048_576; // 1MB + const SlugSchema = z .string() .trim() @@ -57,6 +57,9 @@ const ProjectTemplateRolesSchema = z .superRefine((roles, ctx) => { 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) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" }); @@ -81,6 +84,9 @@ const ProjectTemplateEnvironmentsSchema = z .array() .min(1) .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) 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) => { 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({ ...req.auditLogInfo, @@ -153,7 +159,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const projectTemplate = await server.services.projectTemplate.findProjectTemplatesById( + const projectTemplate = await server.services.projectTemplate.findProjectTemplateById( req.params.templateId, req.permission ); @@ -182,10 +188,10 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) schema: { description: "Create a project template.", body: z.object({ - name: SlugSchema.refine((val) => val !== DefaultProjectTemplateIdentifier, { - message: `The project template name "${DefaultProjectTemplateIdentifier}" is reserved.` + name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), { + message: `The requested project template name is reserved.` }).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), environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe( ProjectTemplates.CREATE.environments @@ -224,12 +230,12 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider) description: "Update a project template.", params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }), body: z.object({ - name: SlugSchema.refine((val) => val !== DefaultProjectTemplateIdentifier, { - message: `The project template name "${DefaultProjectTemplateIdentifier}" is reserved.` + name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), { + message: `The requested project template name is reserved.` }) .optional() .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), environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments) }), diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 63e0cf3c3..51090e594 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1663,7 +1663,7 @@ interface DeleteProjectTemplateEvent { interface ApplyProjectTemplateEvent { type: EventType.APPLY_PROJECT_TEMPLATE; metadata: { - templateId: string; + template: string; projectId: string; }; } diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 0864ab33c..60e620f71 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -48,7 +48,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ }, pkiEst: false, enforceMfa: false, - projectTemplates: false + projectTemplates: true }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/project-template/project-template-constants.ts b/backend/src/ee/services/project-template/project-template-constants.ts index fce419926..778de2861 100644 --- a/backend/src/ee/services/project-template/project-template-constants.ts +++ b/backend/src/ee/services/project-template/project-template-constants.ts @@ -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 = [ { name: "Development", slug: "dev", position: 1 }, { name: "Staging", slug: "staging", position: 2 }, { 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 -}); diff --git a/backend/src/ee/services/project-template/project-template-fns.ts b/backend/src/ee/services/project-template/project-template-fns.ts new file mode 100644 index 000000000..2ca78e876 --- /dev/null +++ b/backend/src/ee/services/project-template/project-template-fns.ts @@ -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); diff --git a/backend/src/ee/services/project-template/project-template-service.ts b/backend/src/ee/services/project-template/project-template-service.ts index b51c19044..5afa58caf 100644 --- a/backend/src/ee/services/project-template/project-template-service.ts +++ b/backend/src/ee/services/project-template/project-template-service.ts @@ -5,7 +5,7 @@ import { TProjectTemplates } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-constants"; +import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { TCreateProjectTemplateDTO, TProjectTemplateEnvironment, @@ -50,6 +50,13 @@ export const projectTemplateServiceFactory = ({ projectTemplateDAL }: TProjectTemplatesServiceFactoryDep) => { 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( actor.type, actor.id, @@ -60,13 +67,6 @@ export const projectTemplateServiceFactory = ({ 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({ orgId: actor.orgId }); @@ -77,17 +77,7 @@ export const projectTemplateServiceFactory = ({ ]; }; - const findProjectTemplatesById = 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.Read, OrgPermissionSubjects.ProjectTemplates); - + const findProjectTemplateByName = async (name: string, actor: OrgServiceActor) => { const plan = await licenseService.getPlan(actor.orgId); 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." }); - const [projectTemplate] = await projectTemplateDAL.find({ - id, - orgId: actor.orgId // ensure from user org - }); + const projectTemplate = await projectTemplateDAL.findOne({ name, orgId: actor.orgId }); + + 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}` }); + 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 @@ -112,6 +137,13 @@ export const projectTemplateServiceFactory = ({ { roles, environments, ...params }: TCreateProjectTemplateDTO, 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( actor.type, actor.id, @@ -122,11 +154,16 @@ export const projectTemplateServiceFactory = ({ 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({ - 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({ @@ -144,16 +181,6 @@ export const projectTemplateServiceFactory = ({ { roles, environments, ...params }: TUpdateProjectTemplateDTO, 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); 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." }); - 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, roles: roles ? JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))) @@ -169,20 +224,10 @@ export const projectTemplateServiceFactory = ({ environments: environments ? JSON.stringify(environments) : undefined }); - return $unpackProjectTemplate(projectTemplate); + return $unpackProjectTemplate(updatedProjectTemplate); }; 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); 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." }); - 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 { @@ -200,6 +259,7 @@ export const projectTemplateServiceFactory = ({ createProjectTemplate, updateProjectTemplateById, deleteProjectTemplateById, - findProjectTemplatesById + findProjectTemplateById, + findProjectTemplateByName }; }; diff --git a/backend/src/ee/services/project-template/project-template-types.ts b/backend/src/ee/services/project-template/project-template-types.ts index c07187ff4..6b600f386 100644 --- a/backend/src/ee/services/project-template/project-template-types.ts +++ b/backend/src/ee/services/project-template/project-template-types.ts @@ -22,3 +22,7 @@ export type TCreateProjectTemplateDTO = { export type TUpdateProjectTemplateDTO = Partial; export type TUnpackedPermission = z.infer; + +export enum InfisicalProjectTemplate { + Default = "default" +} diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 7038c46ef..136a4db29 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -392,7 +392,7 @@ export const PROJECTS = { organizationSlug: "The slug of the organization to create the project in.", projectName: "The name of the project to create.", 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: { workspaceId: "The ID of the project to delete." diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 7b4e8a93a..c2aa446b4 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -9,6 +9,7 @@ import { ProjectKeysSchema } 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"; import { PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -170,7 +171,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { .optional() .describe(PROJECTS.CREATE.slug), 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: { 200: z.object({ @@ -188,7 +196,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { workspaceName: req.body.projectName, slug: req.body.slug, kmsKeyId: req.body.kmsKeyId, - templateId: req.body.templateId + template: req.body.template }); 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({ ...req.auditLogInfo, orgId: req.permission.orgId, event: { type: EventType.APPLY_PROJECT_TEMPLATE, metadata: { - templateId: req.body.templateId, + template: req.body.template, projectId: project.id } } diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 0542cf47a..dfe2ce3ec 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -7,6 +7,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; 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 { isAtLeastAsPrivileged } from "@app/lib/casl"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; @@ -152,7 +153,7 @@ export const projectServiceFactory = ({ kmsKeyId, tx: trx, createDefaultEnvs = true, - templateId + template = InfisicalProjectTemplate.Default }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); @@ -187,23 +188,19 @@ export const projectServiceFactory = ({ } } - let projectTemplate: Awaited> | null = null; + let projectTemplate: Awaited> | null = null; - if (templateId) { - if (!plan.projectTemplates) - throw new BadRequestError({ - message: - "Failed to apply project template due to plan restriction. Upgrade plan to access project templates." + switch (template) { + case InfisicalProjectTemplate.Default: + projectTemplate = null; + break; + 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( diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 99f8708fe..28cda2d95 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -32,7 +32,7 @@ export type TCreateProjectDTO = { slug?: string; kmsKeyId?: string; createDefaultEnvs?: boolean; - templateId?: string; + template?: string; tx?: Knex; }; diff --git a/docs/documentation/platform/project-templates.mdx b/docs/documentation/platform/project-templates.mdx index d7648a58f..d84c6bdc1 100644 --- a/docs/documentation/platform/project-templates.mdx +++ b/docs/documentation/platform/project-templates.mdx @@ -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. - 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 @@ -115,7 +115,7 @@ In the following steps, we'll explore how to use a project template when creatin --header 'Content-Type: application/json' \ --data '{ "projectName": "My Project", - "templateId": "", + "template": "", // defaults to "default" }' ``` diff --git a/frontend/src/hooks/api/projectTemplates/types.ts b/frontend/src/hooks/api/projectTemplates/types.ts index 82b5970f8..37c6cc902 100644 --- a/frontend/src/hooks/api/projectTemplates/types.ts +++ b/frontend/src/hooks/api/projectTemplates/types.ts @@ -26,4 +26,6 @@ export type TDeleteProjectTemplateDTO = { templateId: string; }; -export const DefaultProjectTemplateIdentifier = "default"; +export enum InfisicalProjectTemplate { + Default = "default" +} diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index c48b7378b..f721d7408 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -209,20 +209,20 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) => export const createWorkspace = ({ projectName, kmsKeyId, - templateId + template }: 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 = () => { const queryClient = useQueryClient(); return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({ - mutationFn: async ({ projectName, kmsKeyId, templateId }) => + mutationFn: async ({ projectName, kmsKeyId, template }) => createWorkspace({ projectName, kmsKeyId, - templateId + template }), onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 2a45d9b85..b763eb8e3 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -57,7 +57,7 @@ export type TGetUpgradeProjectStatusDTO = { export type CreateWorkspaceDTO = { projectName: string; kmsKeyId?: string; - templateId?: string; + template?: string; }; export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index e9337f19c..920c9eaca 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -79,10 +79,7 @@ import { useSelectOrganization } from "@app/hooks/api"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; -import { - DefaultProjectTemplateIdentifier, - useListProjectTemplates -} from "@app/hooks/api/projectTemplates"; +import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; import { Workspace } from "@app/hooks/api/types"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; 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"), addMembers: yup.bool().required().label("Add Members"), 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; @@ -288,12 +285,7 @@ export const AppLayout = ({ children }: LayoutProps) => { enabled: canReadProjectTemplates && subscription?.projectTemplates }); - const onCreateProject = async ({ - name, - addMembers, - kmsKeyId, - templateId - }: TAddProjectFormData) => { + const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => { // type check if (!currentOrg) return; if (!user) return; @@ -305,7 +297,7 @@ export const AppLayout = ({ children }: LayoutProps) => { } = await createWs.mutateAsync({ projectName: name, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, - templateId: templateId === DefaultProjectTemplateIdentifier ? undefined : templateId + template }); if (addMembers) { @@ -948,7 +940,7 @@ export const AppLayout = ({ children }: LayoutProps) => { /> ( { } > )} diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index d7daa1bf3..89aec519e 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -70,10 +70,7 @@ import { useRegisterUserAction } from "@app/hooks/api"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; -import { - DefaultProjectTemplateIdentifier, - useListProjectTemplates -} from "@app/hooks/api/projectTemplates"; +import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { Workspace } from "@app/hooks/api/types"; @@ -488,7 +485,7 @@ const formSchema = yup.object({ .max(64, "Too long, maximum length is 64 characters"), addMembers: yup.bool().required().label("Add Members"), 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; @@ -543,12 +540,7 @@ const OrganizationPage = () => { enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms) }); - const onCreateProject = async ({ - name, - addMembers, - kmsKeyId, - templateId - }: TAddProjectFormData) => { + const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => { // type check if (!currentOrg) return; if (!user) return; @@ -560,7 +552,7 @@ const OrganizationPage = () => { } = await createWs.mutateAsync({ projectName: name, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, - templateId: templateId === DefaultProjectTemplateIdentifier ? undefined : templateId + template }); if (addMembers) { @@ -1076,7 +1068,7 @@ const OrganizationPage = () => { /> ( { } > )} diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx index c52440e32..0923a9bb4 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/EditProjectTemplateSection.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, EmptyState, Spinner } from "@app/components/v2"; import { - DefaultProjectTemplateIdentifier, + InfisicalProjectTemplate, TProjectTemplate, useGetProjectTemplateById } from "@app/hooks/api/projectTemplates"; @@ -16,11 +16,13 @@ type 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, { initialData: template, - enabled: !isDefault + enabled: !isInfisicalTemplate }); return ( @@ -41,7 +43,7 @@ export const EditProjectTemplateSection = ({ template, onBack }: Props) => { ) : projectTemplate ? ( diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx index 820403306..d6b6e0d68 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/EditProjectTemplate.tsx @@ -23,10 +23,10 @@ import { ProjectTemplateRolesSection } from "./ProjectTemplateRolesSection"; type Props = { projectTemplate: TProjectTemplate; 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([ "removeTemplate", "editDetails" @@ -63,7 +63,7 @@ export const EditProjectTemplate = ({ isDefault, projectTemplate, onBack }: Prop

{name}

{description || "Project Template"}

- {!isDefault && ( + {!isInfisicalTemplate && ( )} - + - + )} /> diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx index fa213fc81..fc3b16ccb 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateEnvironmentsForm.tsx @@ -25,7 +25,7 @@ import { slugSchema } from "@app/lib/schemas"; type Props = { projectTemplate: TProjectTemplate; - isDefault: boolean; + isInfisicalTemplate: boolean; }; const formSchema = z.object({ @@ -39,7 +39,10 @@ const formSchema = z.object({ type TFormSchema = z.infer; -export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }: Props) => { +export const ProjectTemplateEnvironmentsForm = ({ + projectTemplate, + isInfisicalTemplate +}: Props) => { const { control, handleSubmit, @@ -94,13 +97,13 @@ export const ProjectTemplateEnvironmentsForm = ({ projectTemplate, isDefault }:

Project Environments

- {!isDefault && ( + {!isInfisicalTemplate && (

Add, rename, remove and reorder environments for this project template

)}
- {!isDefault && ( + {!isInfisicalTemplate && ( Friendly Name Slug - {!isDefault && ( + {!isInfisicalTemplate && (
( - {isDefault ? ( + {isInfisicalTemplate ? ( name ) : ( - {isDefault ? ( + {isInfisicalTemplate ? ( slug ) : ( )} - {!isDefault && ( + {!isInfisicalTemplate && ( { +export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTemplate }: Props) => { const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "removeRole", "editRole" @@ -108,12 +108,12 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isDefaultTemplate

Project Roles

- {isDefaultTemplate + {isInfisicalTemplate ? "Click a role to view the associated permissions" : "Add, edit and remove roles for this project template"}

- {!isDefaultTemplate && ( + {!isInfisicalTemplate && (