Compare commits

..

16 Commits

Author SHA1 Message Date
Scott Wilson
602cf4b3c4 improvement: disable tab select by default on filterable select 2024-11-27 08:40:00 -08:00
Scott Wilson
3b47d7698b improvement: start align components 2024-11-26 15:51:06 -08:00
Scott Wilson
aa9a86df71 improvement: use filter multi-select for adding users to projects on invite 2024-11-26 15:47:23 -08:00
McPizza
92ce05283b feat: Add new tag when creating secret (#2791)
* feat: Add new tag when creating secret
2024-11-26 21:10:14 +01:00
Maidul Islam
39d92ce6ff Merge pull request #2799 from Infisical/misc/finalize-env-default
misc: finalized env schema handling
2024-11-26 15:10:06 -05:00
Sheen Capadngan
44a026446e misc: finalized env schema handling of bool 2024-11-27 04:06:05 +08:00
Scott Wilson
539e5b1907 Merge pull request #2782 from Infisical/fix-remove-payment-method
Fix: Resolve Remove Payment Method Error
2024-11-26 10:54:55 -08:00
Scott Wilson
44b02d5324 Merge pull request #2780 from Infisical/octopus-deploy-integration
Feature: Octopus Deploy Integration
2024-11-26 08:46:25 -08:00
Scott Wilson
71fb6f1d11 Merge branch 'main' into octopus-deploy-integration 2024-11-26 08:36:09 -08:00
Maidul Islam
e64100fab1 Merge pull request #2796 from akhilmhdh/feat/empty-env-stuck
Random patches
2024-11-26 10:54:16 -05:00
=
5bcf07b32b feat: resolved loading screen frozen on no environment and project switch causes forEach undefined error 2024-11-26 20:27:30 +05:30
=
3b0c48052b fix: frontend failing to give token back in cli login 2024-11-26 20:26:14 +05:30
Scott Wilson
b6c05a2f25 improvements: address feedback/requests 2024-11-25 16:35:56 -08:00
Scott Wilson
46ad1d47a9 fix: correct payment ID to remove payment method and add confirmation/notification for removal 2024-11-22 19:47:52 -08:00
Scott Wilson
b762816e66 chore: remove unused lib 2024-11-22 14:58:47 -08:00
Scott Wilson
cf275979ba feature: octopus deploy integration 2024-11-22 14:47:15 -08:00
87 changed files with 2019 additions and 424 deletions

View File

@@ -74,8 +74,8 @@ CAPTCHA_SECRET=
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
OTEL_TELEMETRY_COLLECTION_ENABLED=
OTEL_EXPORT_TYPE=
OTEL_TELEMETRY_COLLECTION_ENABLED=false
OTEL_EXPORT_TYPE=prometheus
OTEL_EXPORT_OTLP_ENDPOINT=
OTEL_OTLP_PUSH_INTERVAL=

View File

@@ -33,6 +33,7 @@
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
@@ -6955,6 +6956,21 @@
"resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.1.0.tgz",
"integrity": "sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w=="
},
"node_modules/@octopusdeploy/api-client": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@octopusdeploy/api-client/-/api-client-3.4.1.tgz",
"integrity": "sha512-j6FRgDNzc6AQoT3CAguYLWxoMR4W5TKCT1BCPpqjEN9mknmdMSKfYORs3djn/Yj/BhqtITTydDpBoREbzKY5+g==",
"license": "Apache-2.0",
"dependencies": {
"adm-zip": "^0.5.9",
"axios": "^1.2.1",
"form-data": "^4.0.0",
"glob": "^8.0.3",
"lodash": "^4.17.21",
"semver": "^7.3.8",
"urijs": "^1.19.11"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -22397,6 +22413,12 @@
"punycode": "^2.1.0"
}
},
"node_modules/urijs": {
"version": "1.19.11",
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
"license": "MIT"
},
"node_modules/url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",

View File

@@ -141,6 +141,7 @@
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
@@ -7,7 +8,6 @@ import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -48,7 +48,15 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
.nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
name: z
.string()
.describe(DYNAMIC_SECRETS.CREATE.name)
.min(1)
.toLowerCase()
.max(64)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
}),
response: {
200: z.object({

View File

@@ -1,8 +1,8 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
import { GROUPS } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -14,7 +14,15 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
slug: slugSchema({ min: 5, max: 36 }).optional().describe(GROUPS.CREATE.slug),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(GROUPS.CREATE.slug),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
}),
response: {
@@ -92,7 +100,14 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
body: z
.object({
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
slug: slugSchema({ min: 5, max: 36 }).describe(GROUPS.UPDATE.slug),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(GROUPS.UPDATE.slug),
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
})
.partial(),

View File

@@ -8,7 +8,6 @@ import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import {
ProjectPermissionSchema,
@@ -34,7 +33,17 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
@@ -68,7 +77,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
@@ -94,7 +103,17 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
@@ -140,7 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
@@ -170,7 +189,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
privilegeDetails: z
.object({
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission

View File

@@ -1,8 +1,8 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -18,10 +18,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
organizationId: z.string().trim()
}),
body: z.object({
slug: slugSchema({ min: 1, max: 64 }).refine(
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
),
slug: z
.string()
.min(1)
.trim()
.refine(
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
}),
name: z.string().trim(),
description: z.string().trim().optional(),
permissions: z.any().array()
@@ -87,13 +94,17 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim()
}),
body: z.object({
// TODO: Switch to slugSchema after verifying correct methods with Akhil - Omar 11/24
slug: slugSchema({ min: 1, max: 64 })
slug: z
.string()
.trim()
.optional()
.refine(
(val) => !Object.keys(OrgMembershipRole).includes(val),
(val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved."
)
.optional(),
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
permissions: z.any().array().optional()

View File

@@ -1,4 +1,5 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
@@ -8,7 +9,6 @@ import {
} from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -32,11 +32,18 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
}),
body: z.object({
slug: slugSchema({ max: 64 })
slug: z
.string()
.toLowerCase()
.trim()
.min(1)
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
@@ -87,13 +94,21 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
slug: slugSchema()
slug: z
.string()
.toLowerCase()
.trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
(val) =>
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.describe(PROJECT_ROLE.UPDATE.slug)
.optional(),
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
@@ -7,13 +8,22 @@ import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-tem
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 { slugSchema } from "@app/server/lib/schemas";
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 = 32_768;
const SlugSchema = z
.string()
.trim()
.min(1)
.max(32)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Must be valid slug format"
});
const isReservedRoleSlug = (slug: string) =>
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
@@ -24,14 +34,14 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
roles: z
.object({
name: z.string().trim().min(1),
slug: slugSchema(),
slug: SlugSchema,
permissions: UnpackedPermissionSchema.array()
})
.array(),
environments: z
.object({
name: z.string().trim().min(1),
slug: slugSchema(),
slug: SlugSchema,
position: z.number().min(1)
})
.array()
@@ -40,7 +50,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
const ProjectTemplateRolesSchema = z
.object({
name: z.string().trim().min(1),
slug: slugSchema(),
slug: SlugSchema,
permissions: ProjectPermissionV2Schema.array()
})
.array()
@@ -68,7 +78,7 @@ const ProjectTemplateRolesSchema = z
const ProjectTemplateEnvironmentsSchema = z
.object({
name: z.string().trim().min(1),
slug: slugSchema(),
slug: SlugSchema,
position: z.number().min(1)
})
.array()
@@ -178,11 +188,9 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
schema: {
description: "Create a project template.",
body: z.object({
name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
.describe(ProjectTemplates.CREATE.name),
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
}).describe(ProjectTemplates.CREATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
@@ -222,10 +230,9 @@ 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({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
.optional()
.describe(ProjectTemplates.UPDATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),

View File

@@ -7,7 +7,6 @@ import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/pr
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -22,7 +21,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
schema: {
body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
type: z.discriminatedUnion("isTemporary", [
z.object({
@@ -78,7 +87,15 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
}),
body: z
.object({
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
slug: z
.string()
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),

View File

@@ -7,7 +7,6 @@ import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-p
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -29,7 +28,17 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
type: z.discriminatedUnion("isTemporary", [
z.object({
@@ -91,7 +100,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id)
}),
body: z.object({
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),

View File

@@ -1,11 +1,11 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -29,11 +29,18 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
}),
body: z.object({
slug: slugSchema({ min: 1, max: 64 })
slug: z
.string()
.toLowerCase()
.trim()
.min(1)
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
@@ -83,13 +90,21 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
slug: slugSchema({ min: 1, max: 64 })
slug: z
.string()
.toLowerCase()
.trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
(val) =>
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.optional()
.describe(PROJECT_ROLE.UPDATE.slug),
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()

View File

@@ -1082,7 +1082,8 @@ export const INTEGRATION = {
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
shouldEnableDelete: "The flag to enable deletion of secrets."
shouldEnableDelete: "The flag to enable deletion of secrets.",
octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy."
}
},
UPDATE: {

View File

@@ -10,7 +10,7 @@ export const GITLAB_URL = "https://gitlab.com";
export const IS_PACKAGED = (process as any)?.pkg !== undefined;
const zodStrBool = z
.enum(["true", "false"])
.string()
.optional()
.transform((val) => val === "true");

View File

@@ -1,24 +0,0 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
interface SlugSchemaInputs {
min?: number;
max?: number;
field?: string;
}
export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => {
return z
.string()
.toLowerCase()
.trim()
.min(min, {
message: `${field} field must be at least ${min} character${min === 1 ? "" : "s"}`
})
.max(max, {
message: `${field} field must be at most ${max} character${max === 1 ? "" : "s"}`
})
.refine((v) => slugify(v, { lowercase: true, separator: "-" }) === v, {
message: `${field} field can only contain letters, numbers, and hyphens`
});
};

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
@@ -7,12 +8,19 @@ import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
const keyNameSchema = z
.string()
.trim()
.min(1)
.max(32)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Name must be slug friendly"
});
const keyDescriptionSchema = z.string().trim().max(500).optional();
const base64Schema = z.string().superRefine((val, ctx) => {

View File

@@ -1,9 +1,9 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -48,7 +48,13 @@ export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZ
mappings: z
.object({
groupName: z.string().trim().min(1),
roleSlug: slugSchema({ max: 64 })
roleSlug: z
.string()
.min(1)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Role must be a valid slug"
})
})
.array()
}),

View File

@@ -5,6 +5,7 @@ import { INTEGRATION_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { OctopusDeployScope } from "@app/services/integration-auth/integration-auth-types";
import { integrationAuthPubSchema } from "../sanitizedSchemas";
@@ -1008,4 +1009,118 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
return { buildConfigs };
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/octopus-deploy/scope-values",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
querystring: z.object({
scope: z.nativeEnum(OctopusDeployScope),
spaceId: z.string().trim(),
resourceId: z.string().trim()
}),
response: {
200: z.object({
Environments: z
.object({
Name: z.string(),
Id: z.string()
})
.array(),
Machines: z
.object({
Name: z.string(),
Id: z.string()
})
.array(),
Actions: z
.object({
Name: z.string(),
Id: z.string()
})
.array(),
Roles: z
.object({
Name: z.string(),
Id: z.string()
})
.array(),
Channels: z
.object({
Name: z.string(),
Id: z.string()
})
.array(),
TenantTags: z
.object({
Name: z.string(),
Id: z.string()
})
.array(),
Processes: z
.object({
ProcessType: z.string(),
Name: z.string(),
Id: z.string()
})
.array()
})
}
},
handler: async (req) => {
const scopeValues = await server.services.integrationAuth.getOctopusDeployScopeValues({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
scope: req.query.scope,
spaceId: req.query.spaceId,
resourceId: req.query.resourceId
});
return scopeValues;
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/octopus-deploy/spaces",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
response: {
200: z.object({
spaces: z
.object({
Name: z.string(),
Id: z.string(),
IsDefault: z.boolean()
})
.array()
})
}
},
handler: async (req) => {
const spaces = await server.services.integrationAuth.getOctopusDeploySpaces({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
return { spaces };
}
});
};

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@@ -13,7 +14,6 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
@@ -243,10 +243,22 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
slug: slugSchema({ max: 64 }).optional(),
slug: z
.string()
.trim()
.max(64, { message: "Slug must be 64 or fewer characters" })
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
.optional(),
authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
defaultMembershipRoleSlug: z
.string()
.min(1)
.trim()
.refine((v) => slugify(v) === v, {
message: "Membership role must be a valid slug"
})
.optional(),
enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
}),

View File

@@ -1,10 +1,10 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ENVIRONMENTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -124,7 +124,13 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position),
slug: slugSchema({ max: 64 }).describe(ENVIRONMENTS.CREATE.slug)
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.CREATE.slug)
}),
response: {
200: z.object({
@@ -182,7 +188,14 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
}),
body: z.object({
slug: slugSchema({ max: 64 }).optional().describe(ENVIRONMENTS.UPDATE.slug),
slug: z
.string()
.trim()
.optional()
.refine((v) => !v || slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.UPDATE.slug),
name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
}),

View File

@@ -1,9 +1,9 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas";
import { SECRET_TAGS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -111,7 +111,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}),
body: z.object({
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.CREATE.slug),
slug: z
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.CREATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}),
response: {
@@ -146,7 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}),
body: z.object({
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.UPDATE.slug),
slug: z
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.UPDATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}),
response: {

View File

@@ -1,10 +1,10 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -35,7 +35,12 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
}
],
querystring: z.object({
slug: slugSchema({ max: 64 }),
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
}),
description: z.string().optional()
}),
response: {
@@ -283,7 +288,13 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
id: z.string()
}),
body: z.object({
slug: slugSchema({ max: 64 }).optional(),
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional(),
description: z.string().optional()
}),
response: {

View File

@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@@ -11,7 +12,6 @@ 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 { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -27,6 +27,14 @@ const projectWithEnv = SanitizedProjectSchema.extend({
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
});
const slugSchema = z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be at least 5 character but no more than 36"
});
export const registerProjectRouter = async (server: FastifyZodProvider) => {
/* Get project key */
server.route({
@@ -154,9 +162,21 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
body: z.object({
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
slug: slugSchema({ min: 5, max: 36 }).optional().describe(PROJECTS.CREATE.slug),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional(),
template: slugSchema({ field: "Template Name", max: 64 })
template: z
.string()
.refine((v) => slugify(v) === v, {
message: "Template name must be in slug format"
})
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template)
@@ -224,7 +244,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to delete.")
slug: slugSchema.describe("The slug of the project to delete.")
}),
response: {
200: SanitizedProjectSchema
@@ -258,7 +278,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
slug: slugSchema.describe("The slug of the project to get.")
}),
response: {
200: projectWithEnv
@@ -291,7 +311,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to update.")
slug: slugSchema.describe("The slug of the project to update.")
}),
body: z.object({
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
@@ -334,7 +354,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug)
slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
}),
querystring: z.object({
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
@@ -375,7 +395,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug)
slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
}),
querystring: z.object({
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-await-in-loop */
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { Client as OctopusDeployClient, ProjectRepository as OctopusDeployRepository } from "@octopusdeploy/api-client";
import { TIntegrationAuths } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
@@ -1087,6 +1088,33 @@ const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: strin
return apps;
};
const getAppsOctopusDeploy = async ({
apiKey,
instanceURL,
spaceName = "Default"
}: {
apiKey: string;
instanceURL: string;
spaceName?: string;
}) => {
const client = await OctopusDeployClient.create({
instanceURL,
apiKey,
userAgentApp: "Infisical Integration"
});
const repository = new OctopusDeployRepository(client, spaceName);
const projects = await repository.list({
take: 1000
});
return projects.Items.map((project) => ({
name: project.Name,
appId: project.Id
}));
};
export const getApps = async ({
integration,
integrationAuth,
@@ -1260,6 +1288,13 @@ export const getApps = async ({
orgName: azureDevOpsOrgName as string
});
case Integrations.OCTOPUS_DEPLOY:
return getAppsOctopusDeploy({
apiKey: accessToken,
instanceURL: url!,
spaceName: workspaceSlug
});
default:
throw new NotFoundError({ message: `Integration '${integration}' not found` });
}

View File

@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { Client as OctopusClient, SpaceRepository as OctopusSpaceRepository } from "@octopusdeploy/api-client";
import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
@@ -9,7 +10,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
import { TIntegrationDALFactory } from "../integration/integration-dal";
@@ -20,6 +21,7 @@ import { getApps } from "./integration-app-list";
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import {
OctopusDeployScope,
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
@@ -38,6 +40,8 @@ import {
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO,
TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthOctopusDeployProjectScopeValuesDTO,
TIntegrationAuthOctopusDeploySpacesDTO,
TIntegrationAuthQoveryEnvironmentsDTO,
TIntegrationAuthQoveryOrgsDTO,
TIntegrationAuthQoveryProjectDTO,
@@ -48,6 +52,7 @@ import {
TIntegrationAuthVercelBranchesDTO,
TNorthflankSecretGroup,
TOauthExchangeDTO,
TOctopusDeployVariableSet,
TSaveIntegrationAccessTokenDTO,
TTeamCityBuildConfig,
TVercelBranches
@@ -1521,6 +1526,88 @@ export const integrationAuthServiceFactory = ({
return integrationAuthDAL.create(newIntegrationAuth);
};
const getOctopusDeploySpaces = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TIntegrationAuthOctopusDeploySpacesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const client = await OctopusClient.create({
apiKey: accessToken,
instanceURL: integrationAuth.url!,
userAgentApp: "Infisical Integration"
});
const spaceRepository = new OctopusSpaceRepository(client);
const spaces = await spaceRepository.list({
partialName: "", // throws error if no string is present...
take: 1000
});
return spaces.Items;
};
const getOctopusDeployScopeValues = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id,
scope,
spaceId,
resourceId
}: TIntegrationAuthOctopusDeployProjectScopeValuesDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
let url: string;
switch (scope) {
case OctopusDeployScope.Project:
url = `${integrationAuth.url}/api/${spaceId}/projects/${resourceId}/variables`;
break;
// future support tenant, variable set etc.
default:
throw new InternalServerError({ message: `Unhandled Octopus Deploy scope` });
}
// SDK doesn't support variable set...
const { data: variableSet } = await request.get<TOctopusDeployVariableSet>(url, {
headers: {
"X-NuGet-ApiKey": accessToken,
Accept: "application/json"
}
});
return variableSet.ScopeValues;
};
return {
listIntegrationAuthByProjectId,
listOrgIntegrationAuth,
@@ -1552,6 +1639,8 @@ export const integrationAuthServiceFactory = ({
getBitbucketWorkspaces,
getBitbucketEnvironments,
getIntegrationAccessToken,
duplicateIntegrationAuth
duplicateIntegrationAuth,
getOctopusDeploySpaces,
getOctopusDeployScopeValues
};
};

View File

@@ -193,3 +193,72 @@ export type TIntegrationsWithEnvironment = TIntegrations & {
| null
| undefined;
};
export type TIntegrationAuthOctopusDeploySpacesDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthOctopusDeployProjectScopeValuesDTO = {
id: string;
spaceId: string;
resourceId: string;
scope: OctopusDeployScope;
} & Omit<TProjectPermission, "projectId">;
export enum OctopusDeployScope {
Project = "project"
// add tenant, variable set, etc.
}
export type TOctopusDeployVariableSet = {
Id: string;
OwnerId: string;
Version: number;
Variables: {
Id: string;
Name: string;
Value: string;
Description: string;
Scope: {
Environment?: string[];
Machine?: string[];
Role?: string[];
TargetRole?: string[];
Action?: string[];
User?: string[];
Trigger?: string[];
ParentDeployment?: string[];
Private?: string[];
Channel?: string[];
TenantTag?: string[];
Tenant?: string[];
ProcessOwner?: string[];
};
IsEditable: boolean;
Prompt: {
Description: string;
DisplaySettings: Record<string, string>;
Label: string;
Required: boolean;
} | null;
Type: "String";
IsSensitive: boolean;
}[];
ScopeValues: {
Environments: { Id: string; Name: string }[];
Machines: { Id: string; Name: string }[];
Actions: { Id: string; Name: string }[];
Roles: { Id: string; Name: string }[];
Channels: { Id: string; Name: string }[];
TenantTags: { Id: string; Name: string }[];
Processes: {
ProcessType: string;
Id: string;
Name: string;
}[];
};
SpaceId: string;
Links: {
Self: string;
};
};

View File

@@ -34,7 +34,8 @@ export enum Integrations {
HASURA_CLOUD = "hasura-cloud",
RUNDECK = "rundeck",
AZURE_DEVOPS = "azure-devops",
AZURE_APP_CONFIGURATION = "azure-app-configuration"
AZURE_APP_CONFIGURATION = "azure-app-configuration",
OCTOPUS_DEPLOY = "octopus-deploy"
}
export enum IntegrationType {
@@ -413,6 +414,15 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Octopus Deploy",
slug: "octopus-deploy",
image: "Octopus Deploy.png",
isAvailable: true,
type: "sat",
clientId: "",
docsLink: ""
}
];

View File

@@ -32,14 +32,14 @@ import { z } from "zod";
import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types";
import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
@@ -4201,6 +4201,61 @@ const syncSecretsRundeck = async ({
}
};
const syncSecretsOctopusDeploy = async ({
integration,
integrationAuth,
secrets,
accessToken
}: {
integration: TIntegrations;
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
let url: string;
switch (integration.scope) {
case OctopusDeployScope.Project:
url = `${integrationAuth.url}/api/${integration.targetEnvironmentId}/projects/${integration.appId}/variables`;
break;
// future support tenant, variable set, etc.
default:
throw new InternalServerError({ message: `Unhandled Octopus Deploy scope: ${integration.scope}` });
}
// SDK doesn't support variable set...
const { data: variableSet } = await request.get<TOctopusDeployVariableSet>(url, {
headers: {
"X-NuGet-ApiKey": accessToken,
Accept: "application/json"
}
});
await request.put(
url,
{
...variableSet,
Variables: Object.entries(secrets).map(([key, value]) => ({
Name: key,
Value: value.value,
Description: value.comment ?? "",
Scope:
(integration.metadata as { octopusDeployScopeValues: TOctopusDeployVariableSet["ScopeValues"] })
?.octopusDeployScopeValues ?? {},
IsEditable: false,
Prompt: null,
Type: "String",
IsSensitive: true
}))
} as unknown as TOctopusDeployVariableSet,
{
headers: {
"X-NuGet-ApiKey": accessToken,
Accept: "application/json"
}
}
);
};
/**
* Sync/push [secrets] to [app] in integration named [integration]
*
@@ -4513,6 +4568,14 @@ export const syncIntegrationSecrets = async ({
accessToken
});
break;
case Integrations.OCTOPUS_DEPLOY:
await syncSecretsOctopusDeploy({
integration,
integrationAuth,
secrets,
accessToken
});
break;
default:
throw new BadRequestError({ message: "Invalid integration" });
}

View File

@@ -46,5 +46,18 @@ export const IntegrationMetadataSchema = z.object({
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets),
octopusDeployScopeValues: z
.object({
// in Octopus Deploy Scope Value Format
Environment: z.string().array().optional(),
Action: z.string().array().optional(),
Channel: z.string().array().optional(),
Machine: z.string().array().optional(),
ProcessOwner: z.string().array().optional(),
Role: z.string().array().optional()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.octopusDeployScopeValues)
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View File

@@ -0,0 +1,76 @@
---
title: "Octopus Deploy"
description: "Learn how to sync secrets from Infisical to Octopus Deploy"
---
Prerequisites:
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
<Steps>
<Step title="Create a Service Account for Infisical in Octopus Deploy">
Navigate to **Configuration** > **Users** and click on the **Create Service Account** button.
![integrations octopus deploy
users](/images/integrations/octopus-deploy/integrations-octopus-deploy-user-settings.png)
Fill out the required fields and click on the **Save** button.
![integrations octopus deploy service
account](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-service-account.png)
</Step>
<Step title="Generate an API Key for your Service Account">
On the **Service Account** user page, expand the **API Keys** section and click on the **New API Key** button.
![integrations octopus deploy
new api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-api-key.png)
Fill out the required fields and click on the **Generate New** button.
![integrations octopus deploy
generate api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-generate-api-key.png)
<Note>If you configure your access token to expire,
you will need to generate a new API key for Infisical prior to this date to keep your integration running.</Note>
Copy the generated **API Key** and click on the **Close** button.
![integrations octopus deploy
copy api key](/images/integrations/octopus-deploy/integrations-octopus-deploy-copy-api-key.png)
</Step>
<Step title="Create a Service Accounts Team and assign your Service Account">
<Note>You can skip creating a new team if you already have an Octopus Deploy team configured with
the **Project Contributor** role to assign your Service Account to.</Note>
Navigate to **Configuration** > **Teams** and click on the **Add Team** button.
![integrations octopus deploy
teams](/images/integrations/octopus-deploy/integrations-octopus-deploy-team-settings.png)
Create a new team for **Service Accounts** and click on the **Save** button.
![integrations octopus deploy add
team](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png)
On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button.
![integrations octopus deploy add service account to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-to-team.png)
On the **User Roles** tab, click on the **Include User Role** button, and add the **Project Contributor** role. Optionally,
click on the **Define Scope** button to further refine what projects your Service Account has access to. Click on the **Apply** button once complete.
![integrations octopus deploy add user roles to team](/images/integrations/octopus-deploy/integrations-octopus-deploy-add-role.png)
Save your team changes by clicking on the **Save** button.
![integrations octopus deploy save team changes](/images/integrations/octopus-deploy/integrations-octopus-deploy-save-team.png)
</Step>
<Step title="Setup Integration">
In Infisical, navigate to your **Project** > **Integrations** page and select the **Octopus Deploy** integration.
![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-integrations.png)
Enter your **Instance URL** and **API Key** from **Octopus Deploy** to authorize Infisical.
![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-authorize.png)
Select a **Space** and **Project** from **Octopus Deploy** to sync secrets to; configuring additional **Scope Values** as needed. Click on the **Create Integration** button once configured.
![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-create.png)
Your Infisical secrets will begin to sync to **Octopus Deploy**.
![integration octopus deploy](/images/integrations/octopus-deploy/integrations-octopus-deploy-sync.png)
</Step>
</Steps>

View File

@@ -42,6 +42,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available |
| [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available |
| [Octopus Deploy](/integrations/cicd/octopus-deploy) | CI/CD | Available |
| [React](/integrations/frameworks/react) | Framework | Available |
| [Vue](/integrations/frameworks/vue) | Framework | Available |
| [Express](/integrations/frameworks/express) | Framework | Available |

View File

@@ -423,7 +423,8 @@
"integrations/cicd/travisci",
"integrations/cicd/rundeck",
"integrations/cicd/codefresh",
"integrations/cloud/checkly"
"integrations/cloud/checkly",
"integrations/cicd/octopus-deploy"
]
}
]

View File

@@ -2,7 +2,7 @@ const path = require("path");
const ContentSecurityPolicy = `
default-src 'self';
connect-src 'self' https://*.posthog.com;
connect-src 'self' https://*.posthog.com http://127.0.0.1:*;
script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.com;

View File

@@ -89,7 +89,7 @@
"react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-select": "^5.8.3",
"react-table": "^7.8.0",
"react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1",
@@ -21259,9 +21259,9 @@
}
},
"node_modules/react-select": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz",
"integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",

View File

@@ -162,4 +162,4 @@
"tailwindcss": "3.2",
"typescript": "^4.9.3"
}
}
}

View File

@@ -36,7 +36,8 @@ const integrationSlugNameMapping: Mapping = {
"hasura-cloud": "Hasura Cloud",
rundeck: "Rundeck",
"azure-devops": "Azure DevOps",
"azure-app-configuration": "Azure App Configuration"
"azure-app-configuration": "Azure App Configuration",
"octopus-deploy": "Octopus Deploy"
};
const envMapping: Mapping = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -17,7 +18,6 @@ import {
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateWsTag } from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
export const secretTagsColors = [
{
@@ -88,7 +88,13 @@ type Props = {
};
const createTagSchema = z.object({
slug: slugSchema({ min: 1, field: "Tag Slug" }),
slug: z
.string()
.trim()
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim()
});

View File

@@ -0,0 +1,68 @@
import { GroupBase } from "react-select";
import ReactSelectCreatable, { CreatableProps } from "react-select/creatable";
import { twMerge } from "tailwind-merge";
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
export const CreatableSelect = <T,>({
isMulti,
closeMenuOnSelect,
...props
}: CreatableProps<T, boolean, GroupBase<T>>) => {
return (
<ReactSelectCreatable
isMulti={isMulti}
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
hideSelectedOptions={false}
unstyled
styles={{
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none"
}
}),
multiValueLabel: (base) => ({
...base,
whiteSpace: "normal",
overflow: "visible"
}),
control: (base) => ({
...base,
transition: "none"
})
}}
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
classNames={{
container: () => "w-full font-inter",
control: ({ isFocused }) =>
twMerge(
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
),
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
input: () => "pl-1 py-0.5",
valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
singleValue: () => "leading-7 ml-1",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm",
multiValueRemove: () => "hover:text-red text-bunker-400",
indicatorsContainer: () => "p-1 gap-1",
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
indicatorSeparator: () => "bg-bunker-400",
dropdownIndicator: () => "text-bunker-200 p-1",
menu: () =>
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
option: ({ isFocused, isSelected }) =>
twMerge(
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
isSelected && "text-mineshaft-200",
"hover:cursor-pointer text-xs px-3 py-2"
),
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
}}
{...props}
/>
);
};

View File

@@ -0,0 +1 @@
export * from "./CreatableSelect";

View File

@@ -1,52 +1,14 @@
import Select, {
ClearIndicatorProps,
components,
DropdownIndicatorProps,
MultiValueRemoveProps,
OptionProps,
Props
} from "react-select";
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Select, { Props } from "react-select";
import { twMerge } from "tailwind-merge";
const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
return (
<components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" />
</components.DropdownIndicator>
);
};
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
return (
<components.ClearIndicator {...props}>
<FontAwesomeIcon icon={faCircleXmark} />
</components.ClearIndicator>
);
};
const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<FontAwesomeIcon icon={faXmark} size="xs" />
</components.MultiValueRemove>
);
};
const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</components.Option>
);
};
export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: Props<T>) => (
export const FilterableSelect = <T,>({
isMulti,
closeMenuOnSelect,
tabSelectsValue = false,
...props
}: Props<T>) => (
<Select
isMulti={isMulti}
closeMenuOnSelect={closeMenuOnSelect ?? !isMulti}
@@ -69,6 +31,7 @@ export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: P
transition: "none"
})
}}
tabSelectsValue={tabSelectsValue}
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
classNames={{
container: () => "w-full font-inter",
@@ -79,7 +42,8 @@ export const FilterableSelect = <T,>({ isMulti, closeMenuOnSelect, ...props }: P
),
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
input: () => "pl-1 py-0.5",
valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
valueContainer: () =>
`p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} gap-1`,
singleValue: () => "leading-7 ml-1",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm",

View File

@@ -0,0 +1,45 @@
import {
ClearIndicatorProps,
components,
DropdownIndicatorProps,
MultiValueRemoveProps,
OptionProps
} from "react-select";
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const DropdownIndicator = <T,>(props: DropdownIndicatorProps<T>) => {
return (
<components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" />
</components.DropdownIndicator>
);
};
export const ClearIndicator = <T,>(props: ClearIndicatorProps<T>) => {
return (
<components.ClearIndicator {...props}>
<FontAwesomeIcon icon={faCircleXmark} />
</components.ClearIndicator>
);
};
export const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<FontAwesomeIcon icon={faXmark} size="xs" />
</components.MultiValueRemove>
);
};
export const Option = <T,>({ isSelected, children, ...props }: OptionProps<T>) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</components.Option>
);
};

View File

@@ -17,7 +17,9 @@ import {
Project,
Service,
Team,
TeamCityBuildConfig
TeamCityBuildConfig,
TGetIntegrationAuthOctopusDeployScopeValuesDTO,
TOctopusDeployVariableSetScopeValues
} from "./types";
const integrationAuthKeys = {
@@ -119,7 +121,14 @@ const integrationAuthKeys = {
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const
}) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const,
getIntegrationAuthOctopusDeploySpaces: (integrationAuthId: string) =>
[{ integrationAuthId }, "getIntegrationAuthOctopusDeploySpaces"] as const,
getIntegrationAuthOctopusDeployScopeValues: ({
integrationAuthId,
...params
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
};
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -479,6 +488,28 @@ const fetchIntegrationAuthTeamCityBuildConfigs = async ({
return buildConfigs;
};
const fetchIntegrationAuthOctopusDeploySpaces = async (integrationAuthId: string) => {
const {
data: { spaces }
} = await apiRequest.get<{
spaces: { Name: string; Slug: string; Id: string; IsDefault: boolean }[];
}>(`/api/v1/integration-auth/${integrationAuthId}/octopus-deploy/spaces`);
return spaces;
};
const fetchIntegrationAuthOctopusDeployScopeValues = async ({
integrationAuthId,
scope,
spaceId,
resourceId
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) => {
const { data } = await apiRequest.get<TOctopusDeployVariableSetScopeValues>(
`/api/v1/integration-auth/${integrationAuthId}/octopus-deploy/scope-values`,
{ params: { scope, spaceId, resourceId } }
);
return data;
};
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -487,17 +518,24 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
});
};
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId,
azureDevOpsOrgName,
workspaceSlug
}: {
integrationAuthId: string;
teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string;
}) => {
export const useGetIntegrationAuthApps = (
{
integrationAuthId,
teamId,
azureDevOpsOrgName,
workspaceSlug
}: {
integrationAuthId: string;
teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string;
},
options?: UseQueryOptions<
Awaited<ReturnType<typeof fetchIntegrationAuthApps>>,
unknown,
Awaited<ReturnType<typeof fetchIntegrationAuthApps>>
>
) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
queryFn: () =>
@@ -507,7 +545,7 @@ export const useGetIntegrationAuthApps = ({
azureDevOpsOrgName,
workspaceSlug
}),
enabled: true
...options
});
};
@@ -759,6 +797,27 @@ export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: stri
});
};
export const useGetIntegrationAuthOctopusDeploySpaces = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthOctopusDeploySpaces(integrationAuthId),
queryFn: () => fetchIntegrationAuthOctopusDeploySpaces(integrationAuthId)
});
};
export const useGetIntegrationAuthOctopusDeployScopeValues = (
params: TGetIntegrationAuthOctopusDeployScopeValuesDTO,
options?: UseQueryOptions<
TOctopusDeployVariableSetScopeValues,
unknown,
TOctopusDeployVariableSetScopeValues
>
) =>
useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthOctopusDeployScopeValues(params),
queryFn: () => fetchIntegrationAuthOctopusDeployScopeValues(params),
...options
});
export const useGetIntegrationAuthBitBucketEnvironments = (
{
integrationAuthId,

View File

@@ -99,3 +99,29 @@ export type TDuplicateIntegrationAuthDTO = {
integrationAuthId: string;
projectId: string;
};
export enum OctopusDeployScope {
Project = "project"
// tenant, variable set
}
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
integrationAuthId: string;
spaceId: string;
resourceId: string;
scope: OctopusDeployScope;
};
export type TOctopusDeployVariableSetScopeValues = {
Environments: { Id: string; Name: string }[];
Machines: { Id: string; Name: string }[];
Actions: { Id: string; Name: string }[];
Roles: { Id: string; Name: string }[];
Channels: { Id: string; Name: string }[];
TenantTags: { Id: string; Name: string }[];
Processes: {
ProcessType: string;
Id: string;
Name: string;
}[];
};

View File

@@ -4,7 +4,7 @@ import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace";
import { TCloudIntegration, TIntegrationWithEnv } from "./types";
import { TCloudIntegration, TIntegrationWithEnv, TOctopusDeployScopeValues } from "./types";
export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const,
@@ -87,6 +87,7 @@ export const useCreateIntegration = () => {
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
octopusDeployScopeValues?: TOctopusDeployScopeValues;
};
}) => {
const {

View File

@@ -58,11 +58,21 @@ export type TIntegration = {
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
octopusDeployScopeValues?: TOctopusDeployScopeValues;
awsIamRole?: string;
region?: string;
};
};
export type TOctopusDeployScopeValues = {
Environment?: string[];
Action?: string[];
Channel?: string[];
Machine?: string[];
ProcessOwner?: string[];
Role?: string[];
};
export type TIntegrationWithEnv = TIntegration & {
environment: {
id: string;

View File

@@ -1,7 +1,6 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { slugSchema } from "@app/lib/schemas";
export type Kms = {
id: string;
description: string;
@@ -89,7 +88,13 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
]);
export const AddExternalKmsSchema = z.object({
name: slugSchema({ min: 1, field: "Alias" }),
name: z
.string()
.trim()
.min(1)
.refine((v) => slugify(v) === v, {
message: "Alias must be a valid slug"
}),
description: z.string().trim().optional(),
provider: ExternalKmsInputSchema
});

View File

@@ -52,7 +52,7 @@ export type Invoice = {
};
export type PmtMethod = {
id: string;
_id: string;
brand: string;
exp_month: number;
exp_year: number;

View File

@@ -457,8 +457,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
value={currentWorkspace?.id}
className="w-full bg-mineshaft-600 py-2.5 font-medium [&>*:first-child]:truncate"
onValueChange={(value) => {
router.push(`/project/${value}/secrets/overview`);
localStorage.setItem("projectData.id", value);
// this is not using react query because react query in overview is throwing error when envs are not exact same count
// to reproduce change this back to router.push and switch between two projects with different env count
// look into this on dashboard revamp
window.location.assign(`/project/${value}/secrets/overview`);
}}
position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"

View File

@@ -1,23 +1,12 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
interface SlugSchemaInputs {
min?: number;
max?: number;
field?: string;
}
export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => {
return z
.string()
.trim()
.min(min, {
message: `${field} field must be at least ${min} character${min === 1 ? "" : "s"}`
})
.max(max, {
message: `${field} field must be at most ${max} character${max === 1 ? "" : "s"}`
})
.refine((v) => slugify(v, { lowercase: true, separator: "-" }) === v, {
message: `${field} field can only contain letters, numbers, and hyphens`
});
};
export const slugSchema = z
.string()
.trim()
.min(1)
.max(32)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Invalid slug format"
});

View File

@@ -181,7 +181,7 @@ export default function BitBucketCreateIntegrationPage() {
onChange={onChange}
options={currentWorkspace?.environments}
placeholder="Select a project environment"
isDisabled={!bitbucketWorkspaces?.length}
isDisabled={!currentWorkspace?.environments.length}
/>
</FormControl>
)}

View File

@@ -0,0 +1,138 @@
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { removeTrailingSlash } from "@app/helpers/string";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
const formSchema = z.object({
instanceUrl: z.string().min(1, { message: "Instance URL required" }),
apiKey: z.string().min(1, { message: "API Key required" })
});
type TForm = z.infer<typeof formSchema>;
export default function OctopusDeployIntegrationPage() {
const router = useRouter();
const { mutateAsync, isLoading } = useSaveIntegrationAccessToken();
const { currentWorkspace } = useWorkspace();
const { control, handleSubmit } = useForm<TForm>({
resolver: zodResolver(formSchema)
});
const onSubmit = async ({ instanceUrl, apiKey }: TForm) => {
try {
const integrationAuth = await mutateAsync({
workspaceId: currentWorkspace!.id,
integration: "octopus-deploy",
url: removeTrailingSlash(instanceUrl),
accessToken: apiKey
});
router.push(`/integrations/octopus-deploy/create?integrationAuthId=${integrationAuth.id}`);
} catch (err: any) {
createNotification({
type: "error",
text: err.message ?? "Error authorizing integration"
});
console.error(err);
}
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full items-center justify-center"
>
<Head>
<title>Authorize Octopus Deploy Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="After adding your credentials, you will be prompted to set up an integration for a particular environment and secret path."
>
<div className="flex flex-row items-center">
<div className="inline-flex items-center pb-0.5">
<Image
src="/images/integrations/Octopus Deploy.png"
height={30}
width={30}
alt="Octopus Deploy logo"
/>
</div>
<span className="ml-1.5">Octopus Deploy Integration</span>
<Link href="https://infisical.com/docs/integrations/cloud/octopus-deploy" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Octopus Deploy Instance URL"
errorText={error?.message}
isError={Boolean(error)}
className="px-6"
>
<Input value={value} onChange={onChange} placeholder="https://xxxx.octopus.app" />
</FormControl>
)}
name="instanceUrl"
control={control}
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Octopus Deploy API Key"
errorText={error?.message}
isError={Boolean(error)}
className="px-6"
>
<Input
value={value}
onChange={onChange}
placeholder="API-XXXXXXXXXXXXXXXXXXXXXXXX"
type="password"
/>
</FormControl>
)}
name="apiKey"
control={control}
/>
<Button
type="submit"
colorSchema="primary"
variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6 w-min"
isLoading={isLoading}
isDisabled={isLoading}
>
Connect to Octopus Deploy
</Button>
</Card>
</form>
);
}
OctopusDeployIntegrationPage.requireAuth = true;

View File

@@ -0,0 +1,444 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { SiOctopusdeploy } from "react-icons/si";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Card,
CardTitle,
FilterableSelect,
FormControl,
Spinner
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { useCreateIntegration, useGetIntegrationAuthApps } from "@app/hooks/api";
import {
useGetIntegrationAuthOctopusDeployScopeValues,
useGetIntegrationAuthOctopusDeploySpaces
} from "@app/hooks/api/integrationAuth/queries";
import { OctopusDeployScope } from "@app/hooks/api/integrationAuth/types";
const formSchema = z.object({
scope: z.nativeEnum(OctopusDeployScope),
secretPath: z.string().default("/"),
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
targetSpace: z.object({ Name: z.string(), Id: z.string() }),
targetResource: z.object({ appId: z.string().optional(), name: z.string() }),
targetEnvironments: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
targetRoles: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
targetMachines: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
targetProcesses: z
.object({ Name: z.string(), Id: z.string(), ProcessType: z.string() })
.array()
.optional(),
targetActions: z.object({ Name: z.string(), Id: z.string() }).array().optional(),
targetChannels: z.object({ Name: z.string(), Id: z.string() }).array().optional()
});
type TFormData = z.infer<typeof formSchema>;
export default function OctopusDeployCreateIntegrationPage() {
const router = useRouter();
const createIntegration = useCreateIntegration();
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
resolver: zodResolver(formSchema),
defaultValues: {
secretPath: "/",
scope: OctopusDeployScope.Project
}
});
const integrationAuthId = router.query.integrationAuthId as string;
const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
const { data: octopusDeploySpaces, isLoading: isLoadingOctopusDeploySpaces } =
useGetIntegrationAuthOctopusDeploySpaces((integrationAuthId as string) ?? "");
const currentSpace = watch("targetSpace", octopusDeploySpaces?.[0]);
const currentScope = watch("scope");
const sourceEnv = watch("sourceEnvironment");
const { data: octopusDeployResources, isLoading: isOctopusDeployResourcesLoading } =
useGetIntegrationAuthApps(
{
integrationAuthId,
workspaceSlug: currentSpace?.Name
// scope once we support other resources than project
},
{
enabled: Boolean(currentSpace ?? octopusDeploySpaces?.find((space) => space.IsDefault))
}
);
const currentResource = watch("targetResource", octopusDeployResources?.[0]);
const { data: octopusDeployScopeValues, isLoading: isOctopusDeployScopeValuesLoading } =
useGetIntegrationAuthOctopusDeployScopeValues(
{
integrationAuthId,
spaceId: currentSpace?.Id,
resourceId: currentResource?.appId!,
scope: currentScope
},
{ enabled: Boolean(currentSpace && currentResource) }
);
const onSubmit = async ({
sourceEnvironment,
secretPath,
targetEnvironments,
targetResource,
targetSpace,
targetChannels,
targetActions,
targetMachines,
targetProcesses,
targetRoles,
scope
}: TFormData) => {
try {
await createIntegration.mutateAsync({
integrationAuthId,
isActive: true,
scope,
app: targetResource.name,
appId: targetResource.appId,
targetEnvironment: targetSpace.Name,
targetEnvironmentId: targetSpace.Id,
metadata: {
octopusDeployScopeValues: {
Environment: targetEnvironments?.map(({ Id }) => Id),
Action: targetActions?.map(({ Id }) => Id),
Channel: targetChannels?.map(({ Id }) => Id),
ProcessOwner: targetProcesses?.map(({ Id }) => Id),
Role: targetRoles?.map(({ Id }) => Id),
Machine: targetMachines?.map(({ Id }) => Id)
}
},
sourceEnvironment: sourceEnvironment.slug,
secretPath
});
createNotification({
type: "success",
text: "Successfully created integration"
});
router.push(`/integrations/${currentWorkspace?.id}`);
} catch (err) {
createNotification({
type: "error",
text: "Failed to create integration"
});
console.error(err);
}
};
useEffect(() => {
if (!octopusDeployResources || !octopusDeploySpaces || !currentWorkspace) return;
reset({
targetResource: octopusDeployResources[0],
targetSpace: octopusDeploySpaces.find((space) => space.IsDefault),
sourceEnvironment: currentWorkspace.environments[0],
secretPath: "/",
scope: OctopusDeployScope.Project
});
}, [octopusDeploySpaces, octopusDeployResources, currentWorkspace]);
if (isProjectLoading || isLoadingOctopusDeploySpaces || isOctopusDeployResourcesLoading)
return (
<div className="flex h-full w-full items-center justify-center p-24">
<Spinner />
</div>
);
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full items-center justify-center"
>
<Card className="max-w-4xl rounded-md p-8 pt-4">
<CardTitle className=" text-center">
<SiOctopusdeploy size="1.2rem" className="mr-2 mb-1 inline-block" />
Octopus Deploy Integration
</CardTitle>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="sourceEnvironment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Project Environment"
>
<FilterableSelect
getOptionValue={(option) => option.slug}
value={value}
getOptionLabel={(option) => option.name}
onChange={onChange}
options={currentWorkspace?.environments}
placeholder="Select a project environment"
isDisabled={!currentWorkspace?.environments.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} label="Secrets Path">
<SecretPathInput
placeholder="/"
environment={sourceEnv?.slug}
value={value}
onChange={onChange}
/>
</FormControl>
)}
/>
<div className="col-span-2 flex w-full flex-row items-center pb-2">
<div className="w-full border-t border-mineshaft-500" />
<span className="mx-2 whitespace-nowrap text-xs text-mineshaft-400">Sync To</span>
<div className="w-full border-t border-mineshaft-500" />
</div>
<Controller
control={control}
name="targetSpace"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Octopus Deploy Space"
>
<FilterableSelect
getOptionValue={(option) => option.Id}
value={value}
getOptionLabel={(option) => option.Name}
onChange={onChange}
options={octopusDeploySpaces}
placeholder={
octopusDeploySpaces?.length ? "Select a space..." : "No spaces found..."
}
isDisabled={!octopusDeploySpaces?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetResource"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="capitalize"
label={`Octopus Deploy ${currentScope}`}
>
<FilterableSelect
getOptionValue={(option) => option.appId!}
value={value}
getOptionLabel={(option) => option.name}
onChange={onChange}
options={octopusDeployResources}
placeholder={
octopusDeployResources?.length ? "Select a project..." : "No projects found..."
}
isDisabled={!octopusDeployResources?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetEnvironments"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Octopus Deploy Environments"
isOptional
>
<FilterableSelect
isMulti
getOptionValue={(option) => option.Name}
value={value}
getOptionLabel={(option) => option.Name}
onChange={onChange}
isLoading={isOctopusDeployScopeValuesLoading}
options={octopusDeployScopeValues?.Environments}
placeholder={
octopusDeployScopeValues?.Environments?.length
? "Select environments..."
: "No environments found..."
}
isDisabled={!octopusDeployScopeValues?.Environments?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetRoles"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Octopus Deploy Target Tags"
isOptional
>
<FilterableSelect
isMulti
getOptionValue={(option) => option.Name}
value={value}
getOptionLabel={(option) => option.Name}
onChange={onChange}
isLoading={isOctopusDeployScopeValuesLoading}
options={octopusDeployScopeValues?.Roles}
placeholder={
octopusDeployScopeValues?.Roles?.length
? "Select target tags..."
: "No target tags found..."
}
isDisabled={!octopusDeployScopeValues?.Roles?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetMachines"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Octopus Deploy Targets"
isOptional
>
<FilterableSelect
isMulti
getOptionValue={(option) => option.Name}
value={value}
getOptionLabel={(option) => option.Name}
onChange={onChange}
isLoading={isOctopusDeployScopeValuesLoading}
options={octopusDeployScopeValues?.Machines}
placeholder={
octopusDeployScopeValues?.Machines?.length
? "Select targets..."
: "No targets found..."
}
isDisabled={!octopusDeployScopeValues?.Machines?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetProcesses"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Octopus Deploy Processes"
isOptional
>
<FilterableSelect
isMulti
getOptionValue={(option) => option.Name}
value={value}
getOptionLabel={(option) => option.Name}
onChange={onChange}
isLoading={isOctopusDeployScopeValuesLoading}
options={octopusDeployScopeValues?.Processes}
placeholder={
octopusDeployScopeValues?.Processes?.length
? "Select processes..."
: "No processes found..."
}
isDisabled={!octopusDeployScopeValues?.Processes?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetActions"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Octopus Deploy Deployment Steps"
isOptional
>
<FilterableSelect
isMulti
getOptionValue={(option) => option.Name}
value={value}
getOptionLabel={(option) => option.Name}
onChange={onChange}
isLoading={isOctopusDeployScopeValuesLoading}
options={octopusDeployScopeValues?.Actions}
placeholder={
octopusDeployScopeValues?.Actions?.length
? "Select deployment steps..."
: "No deployment steps found..."
}
isDisabled={!octopusDeployScopeValues?.Actions?.length}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="targetChannels"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
label="Octopus Deploy Channels"
isOptional
>
<FilterableSelect
isMulti
getOptionValue={(option) => option.Name}
value={value}
getOptionLabel={(option) => option.Name}
onChange={onChange}
isLoading={isOctopusDeployScopeValuesLoading}
options={octopusDeployScopeValues?.Channels}
placeholder={
octopusDeployScopeValues?.Channels?.length
? "Select channels..."
: "No channels found..."
}
isDisabled={!octopusDeployScopeValues?.Channels?.length}
/>
</FormControl>
)}
/>
</div>
<Button
type="submit"
colorSchema="primary"
className="mt-4"
isLoading={createIntegration.isLoading}
isDisabled={createIntegration.isLoading || !octopusDeployResources?.length}
>
Create Integration
</Button>
</Card>
</form>
);
}
OctopusDeployCreateIntegrationPage.requireAuth = true;

View File

@@ -55,6 +55,8 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
return "Path";
case "bitbucket":
return "Repository";
case "octopus-deploy":
return "Project";
case "github":
if (["github-env", "github-repo"].includes(integration.scope!)) {
return "Repository";
@@ -104,6 +106,16 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
</div>
);
}
if (integration.integration === "octopus-deploy") {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Space" />
<div className="text-sm text-mineshaft-300">{integration.targetEnvironment}</div>
</div>
);
}
if (
["vercel", "netlify", "railway", "gitlab", "teamcity"].includes(integration.integration) ||
(integration.integration === "github" && integration.scope === "github-env")

View File

@@ -1,4 +1,5 @@
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
import { OctopusDeployScopeValues } from "@app/views/IntegrationsPage/IntegrationDetailsPage/components/OctopusDeployScopeValues";
type Props = {
integration: TIntegrationWithEnv;
@@ -27,6 +28,7 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
shouldProtectSecrets: "GitLab Secret Protection Enabled",
shouldEnableDelete: "GitHub Secret Deletion Enabled",
octopusDeployScopeValues: "Octopus Deploy Scope Values",
awsIamRole: "AWS IAM Role",
region: "Region"
} as const;
@@ -35,6 +37,9 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
const renderValue = <K extends MetadataKey>(key: K, value: MetadataValue<K>) => {
if (!value) return null;
if (key === "octopusDeployScopeValues")
return <OctopusDeployScopeValues integration={integration} />;
// If it's a boolean, we render a generic "Yes" or "No" response.
if (typeof value === "boolean") {
return value ? "Yes" : "No";
@@ -51,7 +56,7 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
}
if (key === "githubVisibilityRepoIds") {
return value.join(", ");
return (value as string[]).join(", ");
}
}

View File

@@ -0,0 +1,90 @@
import { FormLabel, Spinner } from "@app/components/v2";
import { useGetIntegrationAuthOctopusDeployScopeValues } from "@app/hooks/api/integrationAuth/queries";
import {
OctopusDeployScope,
TOctopusDeployVariableSetScopeValues
} from "@app/hooks/api/integrationAuth/types";
import { TIntegration, TOctopusDeployScopeValues } from "@app/hooks/api/integrations/types";
type OctopusDeployScopeValuesProps = {
integration: TIntegration;
};
// remove plural since Octopus Deploy can decide whether they want to use singular or plural...
const modifyKey = (key: keyof TOctopusDeployVariableSetScopeValues) => {
switch (key) {
case "Processes":
return "ProcessOwner";
default:
return key.substring(0, key.length - 1);
}
};
export const OctopusDeployScopeValues = ({ integration }: OctopusDeployScopeValuesProps) => {
const hasScopeValues = Boolean(
Object.values(integration.metadata?.octopusDeployScopeValues ?? {}).some(
(values) => values.length > 0
)
);
const { data: scopeValues = {}, isLoading } = useGetIntegrationAuthOctopusDeployScopeValues(
{
scope: OctopusDeployScope.Project,
spaceId: integration.targetEnvironmentId!,
resourceId: integration.appId!,
integrationAuthId: integration.integrationAuthId
},
{
enabled: hasScopeValues
}
);
if (!integration.metadata?.octopusDeployScopeValues)
return <span className="text-sm text-mineshaft-400">Not Configured</span>;
if (isLoading) return <Spinner size="sm" className="mt-2 ml-2" />;
const scopeValuesMap = new Map(
Object.entries(scopeValues).map(([key, values]) => [
modifyKey(key as keyof TOctopusDeployVariableSetScopeValues),
new Map((values as { Name: string; Id: string }[]).map((value) => [value.Id, value.Name]))
])
);
return (
<>
{Object.entries(integration.metadata.octopusDeployScopeValues).map(([key, values]) => {
if (!values.length) return null;
const getLabel = (scope: string) => {
switch (scope as keyof TOctopusDeployScopeValues) {
case "Role":
return "Target Tags";
case "Machine":
return "Targets";
case "ProcessOwner":
return "Processes";
case "Action":
return "Steps";
default:
return `${scope}s`;
}
};
return (
<div className="mt-4" key={key}>
<FormLabel className="text-sm font-semibold text-mineshaft-200" label={getLabel(key)} />
<div className="text-sm text-mineshaft-300">
{values
.map((value) => scopeValuesMap.get(key)?.get(value)!)
.map((name) => (
<p key={name}>{name}</p>
))}
</div>
</div>
);
})}
</>
);
};

View File

@@ -140,6 +140,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "azure-devops":
link = `${window.location.origin}/integrations/azure-devops/authorize`;
break;
case "octopus-deploy":
link = `${window.location.origin}/integrations/octopus-deploy/authorize`;
break;
default:
break;
}

View File

@@ -76,6 +76,14 @@ export const ConfiguredIntegrationItem = ({
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
{integration.integration === "octopus-deploy" && (
<div className="ml-2 flex flex-col">
<FormLabel label="Space" />
<div className="overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment || integration.targetEnvironmentId}
</div>
</div>
)}
{integration.integration === "qovery" && (
<div className="flex flex-row">
<div className="ml-2 flex flex-col">
@@ -108,6 +116,7 @@ export const ConfiguredIntegrationItem = ({
(integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "circleci" && "Project") ||
(integration.integration === "bitbucket" && "Repository") ||
(integration.integration === "octopus-deploy" && "Project") ||
(integration.integration === "aws-secret-manager" && "Secret") ||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
(integration?.integration === "terraform-cloud" && "Project") ||

View File

@@ -1,28 +1,18 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import {
faCheckCircle,
faChevronDown,
faExclamationCircle
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FilterableSelect,
FormControl,
Modal,
ModalContent,
Select,
SelectItem,
TextArea,
Tooltip
TextArea
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { isCustomOrgRole } from "@app/helpers/roles";
@@ -44,7 +34,16 @@ const EmailSchema = z.string().email().min(1).trim().toLowerCase();
const addMemberFormSchema = z.object({
emails: z.string().min(1).trim().toLowerCase(),
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
projects: z
.array(
z.object({
name: z.string(),
id: z.string(),
slug: z.string(),
version: z.nativeEnum(ProjectVersion)
})
)
.default([]),
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
});
@@ -72,7 +71,7 @@ export const AddOrgMemberModal = ({
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { data: projects } = useGetUserWorkspaces(true);
const { data: projects, isLoading: isProjectsLoading } = useGetUserWorkspaces(true);
const {
control,
@@ -95,18 +94,14 @@ export const AddOrgMemberModal = ({
}
}, [organizationRoles]);
const selectedProjectIds = watch("projectIds", []);
const onAddMembers = async ({
emails,
organizationRoleSlug,
projectIds,
projects: selectedProjects,
projectRoleSlug
}: TAddMemberForm) => {
if (!currentOrg?.id) return;
const selectedProjects = projects?.filter((project) => projectIds.includes(String(project.id)));
if (selectedProjects?.length) {
// eslint-disable-next-line no-restricted-syntax
for (const project of selectedProjects) {
@@ -144,7 +139,7 @@ export const AddOrgMemberModal = ({
organizationId: currentOrg?.id,
inviteeEmails: emails.split(",").map((email) => email.trim()),
organizationRoleSlug,
projects: projectIds.map((id) => ({ id, projectRoleSlug: [projectRoleSlug] }))
projects: selectedProjects.map(({ id }) => ({ id, projectRoleSlug: [projectRoleSlug] }))
});
setCompleteInviteLinks(data?.completeInviteLinks ?? null);
@@ -182,6 +177,7 @@ export const AddOrgMemberModal = ({
}}
>
<ModalContent
bodyClassName="overflow-visible"
title={`Invite others to ${currentOrg?.name}`}
subTitle={
<div>
@@ -236,98 +232,33 @@ export const AddOrgMemberModal = ({
)}
/>
<div className="flex items-center justify-between gap-2">
<div className="flex items-start justify-between gap-2">
<div className="w-full">
<Controller
control={control}
name="projectIds"
render={({ field, fieldState: { error } }) => (
name="projects"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Assign users to projects (optional)"
label="Assign users to projects"
isOptional
isError={Boolean(error?.message)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{projects && projects.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedProjectIds.length === 1
? projects.find((project) => project.id === selectedProjectIds[0])
?.name
: selectedProjectIds.length === 0
? "No projects selected"
: `${selectedProjectIds.length} projects selected`}
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No projects found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{projects && projects.length > 0 ? (
projects.map((project) => {
const isSelected = selectedProjectIds.includes(String(project.id));
return (
<DropdownMenuItem
onSelect={(event) =>
projects.length > 1 && event.preventDefault()
}
onClick={() => {
if (selectedProjectIds.includes(String(project.id))) {
field.onChange(
selectedProjectIds.filter(
(projectId: string) => projectId !== String(project.id)
)
);
} else {
field.onChange([...selectedProjectIds, String(project.id)]);
}
}}
key={`project-id-${project.id}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
<div className="flex items-center gap-2 capitalize">
{project.name}
{project.version !== ProjectVersion.V3 && (
<Tooltip content="Project is not compatible with this action, please upgrade this project.">
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-xs opacity-50"
/>
</Tooltip>
)}
</div>
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
<FilterableSelect
isMulti
value={value}
onChange={onChange}
isLoading={isProjectsLoading}
getOptionLabel={(project) => project.name}
getOptionValue={(project) => project.id}
options={projects}
placeholder="Select projects..."
/>
</FormControl>
)}
/>
</div>
<div className="flex min-w-fit justify-end">
<div className="mt-[0.15rem] flex min-w-fit justify-end">
<Controller
control={control}
name="projectRoleSlug"
@@ -340,7 +271,7 @@ export const AddOrgMemberModal = ({
>
<div>
<Select
isDisabled={selectedProjectIds.length === 0}
isDisabled={watch("projects", []).length === 0}
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}

View File

@@ -9,13 +9,12 @@ import { Button, FormControl, Input, Modal, ModalContent } from "@app/components
import { useOrganization } from "@app/context";
import { useCreateOrgRole, useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
const schema = z
.object({
name: z.string(),
description: z.string(),
slug: slugSchema({ min: 1 })
slug: z.string()
})
.required();

View File

@@ -1,5 +1,6 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -16,10 +17,16 @@ import {
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { EncryptionAlgorithm, TCmek, useCreateCmek, useUpdateCmek } from "@app/hooks/api/cmeks";
import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({
name: slugSchema({ min: 1, max: 32, field: "Name" }),
name: z
.string()
.min(1)
.toLowerCase()
.max(32)
.refine((v) => slugify(v) === v, {
message: "Name must be in slug format"
}),
description: z.string().max(500).optional(),
encryptionAlgorithm: z.nativeEnum(EncryptionAlgorithm)
});

View File

@@ -13,13 +13,12 @@ import {
useUpdateProjectRole
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
const schema = z
.object({
name: z.string(),
description: z.string(),
slug: slugSchema({ min: 1 })
slug: z.string()
})
.required();

View File

@@ -6,11 +6,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { Button, FormControl, Input } from "@app/components/v2";
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateSecretV3, useGetWsTags } from "@app/hooks/api";
import { useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
import { SecretType } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
@@ -50,12 +51,32 @@ export const CreateSecretForm = ({
const { closePopUp } = usePopUpAction();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const createWsTag = useCreateWsTag();
const { permission } = useProjectPermission();
const canReadTags = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
const { data: projectTags, isLoading: isTagsLoading } = useGetWsTags(
canReadTags ? workspaceId : ""
);
const slugSchema = z.string().trim().toLowerCase().min(1);
const createNewTag = async (slug: string) => {
// TODO: Replace with slugSchema generic
try {
const parsedSlug = slugSchema.parse(slug);
await createWsTag.mutateAsync({
workspaceID: workspaceId,
tagSlug: parsedSlug,
tagColor: ""
});
} catch {
createNotification({
type: "error",
text: "Failed to create new tag"
});
}
};
const handleFormSubmit = async ({ key, value, tags }: TFormSchema) => {
try {
await createSecretV3({
@@ -148,16 +169,18 @@ export const CreateSecretForm = ({
)
}
>
<FilterableSelect
<CreatableSelect
isMulti
className="w-full"
placeholder="Select tags to assign to secret..."
isMulti
isValidNewOption={(v) => slugSchema.safeParse(v).success}
name="tagIds"
isDisabled={!canReadTags}
isLoading={isTagsLoading && canReadTags}
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
value={field.value}
onChange={field.onChange}
onCreateOption={createNewTag}
/>
</FormControl>
)}

View File

@@ -186,7 +186,7 @@ export const SecretOverviewPage = () => {
useGetImportedSecretsAllEnvs({
projectId: workspaceId,
path: secretPath,
environments: userAvailableEnvs.map(({ slug }) => slug)
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const { isLoading: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
@@ -618,7 +618,7 @@ export const SecretOverviewPage = () => {
}
}, [router.query.search]);
if (isWorkspaceLoading || (isProjectV3 && isOverviewLoading)) {
if (isWorkspaceLoading || (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading)) {
return (
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
<img

View File

@@ -7,15 +7,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormControl,
FormLabel,
Input,
Tooltip
} from "@app/components/v2";
import { Button, Checkbox, FormControl, FormLabel, Input, Tooltip } from "@app/components/v2";
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import {
ProjectPermissionActions,
@@ -27,6 +20,7 @@ import { getKeyValue } from "@app/helpers/parseEnvVar";
import {
useCreateFolder,
useCreateSecretV3,
useCreateWsTag,
useGetWsTags,
useUpdateSecretV3
} from "@app/hooks/api";
@@ -199,6 +193,25 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
setValue("value", value);
};
const createWsTag = useCreateWsTag();
const slugSchema = z.string().trim().toLowerCase().min(1);
const createNewTag = async (slug: string) => {
// TODO: Replace with slugSchema generic
try {
const parsedSlug = slugSchema.parse(slug);
await createWsTag.mutateAsync({
workspaceID: workspaceId,
tagSlug: parsedSlug,
tagColor: ""
});
} catch {
createNotification({
type: "error",
text: "Failed to create new tag"
});
}
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
<FormControl
@@ -249,16 +262,18 @@ export const CreateSecretForm = ({ secretPath = "/", getSecretByKey, onClose }:
)
}
>
<FilterableSelect
className="w-full"
placeholder="Select tags to assign to secrets..."
<CreatableSelect
isMulti
className="w-full"
placeholder="Select tags to assign to secret..."
isValidNewOption={(v) => slugSchema.safeParse(v).success}
name="tagIds"
isDisabled={!canReadTags}
isLoading={isTagsLoading && canReadTags}
options={projectTags?.map((el) => ({ label: el.slug, value: el.id }))}
value={field.value}
onChange={field.onChange}
onCreateOption={createNewTag}
/>
</FormControl>
)}

View File

@@ -1,8 +1,10 @@
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
EmptyState,
IconButton,
Table,
@@ -15,76 +17,101 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
export const PmtMethodsTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?.id ?? "");
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
const { handlePopUpOpen, handlePopUpClose, handlePopUpToggle, popUp } = usePopUp([
"removeCard"
] as const);
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
if (!currentOrg?.id) return;
await deleteOrgPmtMethod.mutateAsync({
organizationId: currentOrg.id,
pmtMethodId
});
const pmtMethodToRemove = popUp.removeCard.data as { id: string; last4: string } | undefined;
const handleDeletePmtMethodBtnClick = async () => {
if (!currentOrg?.id || !pmtMethodToRemove) return;
try {
await deleteOrgPmtMethod.mutateAsync({
organizationId: currentOrg.id,
pmtMethodId: pmtMethodToRemove.id
});
createNotification({
type: "success",
text: "Successfully removed payment method"
});
handlePopUpClose("removeCard");
} catch (error: any) {
createNotification({
type: "error",
text: error.message ?? "Error removing payment method"
});
}
};
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Brand</Th>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Last 4 Digits</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ id, brand, exp_month, exp_year, funding, last4 }) => (
<Tr key={`pmt-method-${id}`} className="h-10">
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
<Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<IconButton
onClick={async () => {
await handleDeletePmtMethodBtnClick(id);
}}
size="lg"
isDisabled={!isAllowed}
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
<>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Brand</Th>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Last 4 Digits</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ _id: id, brand, exp_month, exp_year, funding, last4 }) => (
<Tr key={`pmt-method-${id}`} className="h-10">
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
<Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (
<IconButton
onClick={() => handlePopUpOpen("removeCard", { id, last4 })}
size="lg"
isDisabled={!isAllowed}
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={5} innerKey="pmt-methods" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No payment methods on file" icon={faCreditCard} />
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={5} innerKey="pmt-methods" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No payment methods on file" icon={faCreditCard} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
)}
</TBody>
</Table>
</TableContainer>
<DeleteActionModal
isOpen={popUp.removeCard.isOpen}
deleteKey="confirm"
onChange={(isOpen) => handlePopUpToggle("removeCard", isOpen)}
title={`Remove payment method ending in *${pmtMethodToRemove?.last4}?`}
onDeleteApproved={handleDeletePmtMethodBtnClick}
/>
</>
);
};

View File

@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import axios from "axios";
import { z } from "zod";
@@ -14,7 +15,6 @@ import {
useGetSlackIntegrationById,
useUpdateSlackIntegration
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
type Props = {
id?: string;
@@ -22,7 +22,13 @@ type Props = {
};
const slackFormSchema = z.object({
slug: slugSchema({ min: 1, field: "Alias" }),
slug: z
.string()
.trim()
.min(1)
.refine((v) => slugify(v) === v, {
message: "Alias must be a valid slug"
}),
description: z.string().optional()
});

View File

@@ -31,7 +31,7 @@ type Props = {
};
const formSchema = z.object({
slug: slugSchema(),
slug: slugSchema,
name: z.string().trim().min(1),
permissions: projectRoleFormSchema.shape.permissions
});

View File

@@ -32,7 +32,7 @@ const formSchema = z.object({
environments: z
.object({
name: z.string().trim().min(1),
slug: slugSchema({ min: 1, max: 32 })
slug: slugSchema
})
.array()
});

View File

@@ -1,5 +1,6 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -17,10 +18,17 @@ import {
useCreateProjectTemplate,
useUpdateProjectTemplate
} from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas";
const formSchema = z.object({
name: slugSchema({ min: 1, max: 32, field: "Name" }),
name: z
.string()
.trim()
.min(1)
.max(32)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Name must be in slug format"
}),
description: z.string().max(500).optional()
});

View File

@@ -1,13 +1,13 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { yupResolver } from "@hookform/resolvers/yup";
import slugify from "@sindresorhus/slugify";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateWsEnvironment } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
type Props = {
popUp: UsePopUpState<["createEnv"]>;
@@ -15,20 +15,26 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createEnv"]>, state?: boolean) => void;
};
const schema = z.object({
environmentName: z
const schema = yup.object({
environmentName: yup.string().label("Environment Name").required(),
environmentSlug: yup
.string()
.min(1, { message: "Environment Name field must be at least 1 character" }),
environmentSlug: slugSchema()
.label("Environment Slug")
.test({
test: (slug) => slugify(slug as string) === slug,
message: "Slug must be a valid slug"
})
.required()
});
export type FormData = z.infer<typeof schema>;
export type FormData = yup.InferType<typeof schema>;
export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useCreateWsEnvironment();
const { control, handleSubmit, reset } = useForm<FormData>({
resolver: zodResolver(schema)
resolver: yupResolver(schema)
});
const onFormSubmit = async ({ environmentName, environmentSlug }: FormData) => {
@@ -106,11 +112,7 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
Create
</Button>
<Button
onClick={() => handlePopUpClose("createEnv")}
colorSchema="secondary"
variant="plain"
>
<Button onClick={() => handlePopUpClose("createEnv")} colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>

View File

@@ -1,13 +1,13 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { yupResolver } from "@hookform/resolvers/yup";
import slugify from "@sindresorhus/slugify";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useUpdateWsEnvironment } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
type Props = {
popUp: UsePopUpState<["updateEnv"]>;
@@ -15,18 +15,25 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["updateEnv"]>, state?: boolean) => void;
};
const schema = z.object({
name: z.string(),
slug: slugSchema({ min: 1 })
const schema = yup.object({
name: yup.string().label("Environment Name").required(),
slug: yup
.string()
.label("Environment Slug")
.test({
test: (slug) => slugify(slug as string) === slug,
message: "Slug must be a valid slug"
})
.required()
});
export type FormData = z.infer<typeof schema>;
export type FormData = yup.InferType<typeof schema>;
export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useUpdateWsEnvironment();
const { control, handleSubmit, reset } = useForm<FormData>({
resolver: zodResolver(schema),
resolver: yupResolver(schema),
values: popUp.updateEnv.data as FormData
});

View File

@@ -1,5 +1,6 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -7,10 +8,11 @@ import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@ap
import { useWorkspace } from "@app/context";
import { useCreateWsTag } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
const schema = z.object({
slug: slugSchema({ min: 10, field: "Tag Slug" })
slug: z.string().refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
})
});
export type FormData = z.infer<typeof schema>;