diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index bcef06e10..21f785835 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => { offset = 0, actorId, actorType, + secretPath, eventType, eventMetadata }: Omit & { actorId?: string; actorType?: ActorType; + secretPath?: string; eventType?: EventType[]; eventMetadata?: Record; }, @@ -88,6 +90,10 @@ export const auditLogDALFactory = (db: TDbClient) => { }); } + if (projectId && secretPath) { + void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]); + } + // Filter by actor type if (actorType) { void sqlQuery.where("actor", actorType); diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index ff7dede5f..3b860864f 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -46,10 +46,6 @@ export const auditLogServiceFactory = ({ actorOrgId ); - /** - * NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved - * to the organization level ✅ - */ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); } @@ -64,6 +60,7 @@ export const auditLogServiceFactory = ({ actorId: filter.auditLogActorId, actorType: filter.actorType, eventMetadata: filter.eventMetadata, + secretPath: filter.secretPath, ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 9c19cd3cc..6e8314731 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -32,6 +32,7 @@ export type TListProjectAuditLogDTO = { projectId?: string; auditLogActorId?: string; actorType?: ActorType; + secretPath?: string; eventMetadata?: Record; }; } & Omit; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 800788179..eca11e983 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -828,6 +828,8 @@ export const AUDIT_LOGS = { projectId: "Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", eventType: "The type of the event to export.", + secretPath: + "The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.", userAgentType: "Choose which consuming application to export audit logs for.", eventMetadata: "Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.", diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index ffeb748b8..185738de6 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -1151,6 +1151,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + method: "GET", + url: "/:integrationAuthId/vercel/custom-environments", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + querystring: z.object({ + teamId: z.string().trim() + }), + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + environments: z + .object({ + appId: z.string(), + customEnvironments: z + .object({ + id: z.string(), + slug: z.string() + }) + .array() + }) + .array() + }) + } + }, + handler: async (req) => { + const environments = await server.services.integrationAuth.getVercelCustomEnvironments({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId, + teamId: req.query.teamId + }); + + return { environments }; + } + }); + server.route({ method: "GET", url: "/:integrationAuthId/octopus-deploy/spaces", diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 104898099..db0008ebe 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -11,7 +11,7 @@ import { } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; -import { getLastMidnightDateISO } from "@app/lib/fn"; +import { getLastMidnightDateISO, 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"; @@ -113,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { querystring: z.object({ projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), actorType: z.nativeEnum(ActorType).optional(), + secretPath: z + .string() + .optional() + .transform((val) => (!val ? val : removeTrailingSlash(val))) + .describe(AUDIT_LOGS.EXPORT.secretPath), + // eventType is split with , for multiple values, we need to transform it to array eventType: z .string() diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 316ddcb53..e96a577d9 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { encryptedPrivateKeyIV: z.string().trim(), encryptedPrivateKeyTag: z.string().trim(), salt: z.string().trim(), - verifier: z.string().trim() + verifier: z.string().trim(), + password: z.string().trim() }), response: { 200: z.object({ @@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { userId: token.userId }); - return { message: "Successfully updated backup private key" }; + return { message: "Successfully reset password" }; + } + }); + + server.route({ + method: "POST", + url: "/email/password-setup", + config: { + rateLimit: authRateLimit + }, + schema: { + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + await server.services.password.sendPasswordSetupEmail(req.permission); + + return { + message: "A password setup link has been sent" + }; + } + }); + + server.route({ + method: "POST", + url: "/password-setup", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + protectedKey: z.string().trim(), + protectedKeyIV: z.string().trim(), + protectedKeyTag: z.string().trim(), + encryptedPrivateKey: z.string().trim(), + encryptedPrivateKeyIV: z.string().trim(), + encryptedPrivateKeyTag: z.string().trim(), + salt: z.string().trim(), + verifier: z.string().trim(), + password: z.string().trim(), + token: z.string().trim() + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req, res) => { + await server.services.password.setupPassword(req.body, req.permission); + + const appCfg = getConfig(); + void res.cookie("jid", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + return { message: "Successfully setup password" }; } }); }; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index c0bb7dc17..d15fa4543 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => { const expiresAt = new Date(new Date().getTime() + 86400000); return { token, expiresAt }; } + case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: { + // generate random hex + const token = crypto.randomBytes(16).toString("hex"); + const expiresAt = new Date(new Date().getTime() + 86400000); + return { token, expiresAt }; + } case TokenType.TOKEN_USER_UNLOCK: { const token = crypto.randomBytes(16).toString("hex"); const expiresAt = new Date(new Date().getTime() + 259200000); diff --git a/backend/src/services/auth-token/auth-token-types.ts b/backend/src/services/auth-token/auth-token-types.ts index 65d16850a..5f5843bc6 100644 --- a/backend/src/services/auth-token/auth-token-types.ts +++ b/backend/src/services/auth-token/auth-token-types.ts @@ -6,6 +6,7 @@ export enum TokenType { TOKEN_EMAIL_MFA = "emailMfa", TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation", TOKEN_EMAIL_PASSWORD_RESET = "passwordReset", + TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup", TOKEN_USER_UNLOCK = "userUnlock" } diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 9ed9951fe..9f004eafc 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -4,6 +4,8 @@ import jwt from "jsonwebtoken"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; +import { BadRequestError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; @@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TAuthDALFactory } from "./auth-dal"; -import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type"; -import { AuthTokenType } from "./auth-type"; +import { + TChangePasswordDTO, + TCreateBackupPrivateKeyDTO, + TResetPasswordViaBackupKeyDTO, + TSetupPasswordViaBackupKeyDTO +} from "./auth-password-type"; +import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; type TAuthPasswordServiceFactoryDep = { authDAL: TAuthDALFactory; @@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({ verifier, encryptedPrivateKeyIV, encryptedPrivateKeyTag, - userId + userId, + password }: TResetPasswordViaBackupKeyDTO) => { + const cfg = getConfig(); + + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + await userDAL.updateUserEncryptionByUserId(userId, { encryptionVersion: 2, protectedKey, @@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({ iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, salt, - verifier + verifier, + hashedPassword }); await userDAL.updateById(userId, { @@ -267,6 +280,108 @@ export const authPaswordServiceFactory = ({ return backupKey; }; + const sendPasswordSetupEmail = async (actor: OrgServiceActor) => { + if (actor.type !== ActorType.USER) + throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` }); + + const user = await userDAL.findById(actor.id); + + if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` }); + + if (!user.isAccepted || !user.authMethods) + throw new BadRequestError({ message: `You must complete signup to set a password` }); + + const cfg = getConfig(); + + const token = await tokenService.createTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, + userId: user.id + }); + + const email = user.email ?? user.username; + + await smtpService.sendMail({ + template: SmtpTemplates.SetupPassword, + recipients: [email], + subjectLine: "Infisical Password Setup", + substitutions: { + email, + token, + callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : "" + } + }); + }; + + const setupPassword = async ( + { + encryptedPrivateKey, + protectedKeyTag, + protectedKey, + protectedKeyIV, + salt, + verifier, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + password, + token + }: TSetupPasswordViaBackupKeyDTO, + actor: OrgServiceActor + ) => { + try { + await tokenService.validateTokenForUser({ + type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, + userId: actor.id, + code: token + }); + } catch (e) { + throw new BadRequestError({ message: "Expired or invalid token. Please try again." }); + } + + await userDAL.transaction(async (tx) => { + const user = await userDAL.findById(actor.id, tx); + + if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` }); + + if (!user.isAccepted || !user.authMethods) + throw new BadRequestError({ message: `You must complete signup to set a password` }); + + if (!user.authMethods.includes(AuthMethod.EMAIL)) { + await userDAL.updateById( + actor.id, + { + authMethods: [...user.authMethods, AuthMethod.EMAIL] + }, + tx + ); + } + + const cfg = getConfig(); + + const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND); + + await userDAL.updateUserEncryptionByUserId( + actor.id, + { + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier, + hashedPassword, + serverPrivateKey: null, + clientPublicKey: null + }, + tx + ); + }); + + await tokenService.revokeAllMySessions(actor.id); + }; + return { generateServerPubKey, changePassword, @@ -274,6 +389,8 @@ export const authPaswordServiceFactory = ({ sendPasswordResetEmail, verifyPasswordResetEmail, createBackupPrivateKey, - getBackupPrivateKeyOfUser + getBackupPrivateKeyOfUser, + sendPasswordSetupEmail, + setupPassword }; }; diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index a52374506..7c67c0934 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = { encryptedPrivateKeyTag: string; salt: string; verifier: string; + password: string; +}; + +export type TSetupPasswordViaBackupKeyDTO = { + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + password: string; + token: string; }; export type TCreateBackupPrivateKeyDTO = { diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 3b8078cc1..126037895 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -132,16 +132,26 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { /** * Return list of names of apps for Vercel integration + * This is re-used for getting custom environments for Vercel */ -const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => { - const apps: Array<{ name: string; appId: string }> = []; +export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => { + const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = []; const limit = "20"; let hasMorePages = true; let next: number | null = null; interface Response { - projects: { name: string; id: string }[]; + projects: { + name: string; + id: string; + customEnvironments?: { + id: string; + type: string; + description: string; + slug: string; + }[]; + }[]; pagination: { count: number; next: number | null; @@ -173,7 +183,12 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; data.projects.forEach((a) => { apps.push({ name: a.name, - appId: a.id + appId: a.id, + customEnvironments: + a.customEnvironments?.map((env) => ({ + slug: env.slug, + id: env.id + })) ?? [] }); }); diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index d96643a19..61f753348 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -25,11 +25,12 @@ import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; -import { getApps } from "./integration-app-list"; +import { getApps, getAppsVercel } from "./integration-app-list"; import { TCircleCIContext } from "./integration-app-types"; import { TIntegrationAuthDALFactory } from "./integration-auth-dal"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { + GetVercelCustomEnvironmentsDTO, OctopusDeployScope, TBitbucketEnvironment, TBitbucketWorkspace, @@ -1825,6 +1826,41 @@ export const integrationAuthServiceFactory = ({ return integrationAuthDAL.create(newIntegrationAuth); }; + const getVercelCustomEnvironments = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + teamId, + id + }: GetVercelCustomEnvironmentsDTO) => { + 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, + projectId: integrationAuth.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + + const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + const vercelApps = await getAppsVercel({ + accessToken, + teamId + }); + + return vercelApps.map((app) => ({ + customEnvironments: app.customEnvironments, + appId: app.appId + })); + }; + const getOctopusDeploySpaces = async ({ actorId, actor, @@ -1944,6 +1980,7 @@ export const integrationAuthServiceFactory = ({ getIntegrationAccessToken, duplicateIntegrationAuth, getOctopusDeploySpaces, - getOctopusDeployScopeValues + getOctopusDeployScopeValues, + getVercelCustomEnvironments }; }; diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 68d7bf5b9..8efe6b851 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = { Self: string; }; }; + +export type GetVercelCustomEnvironmentsDTO = { + teamId: string; + id: string; +} & Omit; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 96e8ec598..249307f8d 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -1450,9 +1450,13 @@ const syncSecretsVercel = async ({ secrets: Record; accessToken: string; }) => { + const isCustomEnvironment = !["development", "preview", "production"].includes( + integration.targetEnvironment as string + ); interface VercelSecret { id?: string; type: string; + customEnvironmentIds?: string[]; key: string; value: string; target: string[]; @@ -1486,6 +1490,16 @@ const syncSecretsVercel = async ({ } ) ).data.envs.filter((secret) => { + if (isCustomEnvironment) { + if (!secret.customEnvironmentIds?.includes(integration.targetEnvironment as string)) { + // case: secret does not have the same custom environment + return false; + } + + // no need to check for preview environment, as custom environments are not available in preview + return true; + } + if (!secret.target.includes(integration.targetEnvironment as string)) { // case: secret does not have the same target environment return false; @@ -1583,7 +1597,13 @@ const syncSecretsVercel = async ({ key, value: infisicalSecrets[key]?.value, type: "encrypted", - target: [integration.targetEnvironment as string], + ...(isCustomEnvironment + ? { + customEnvironmentIds: [integration.targetEnvironment as string] + } + : { + target: [integration.targetEnvironment as string] + }), ...(integration.path ? { gitBranch: integration.path @@ -1607,9 +1627,19 @@ const syncSecretsVercel = async ({ key, value: infisicalSecrets[key]?.value, type: res[key].type, - target: res[key].target.includes(integration.targetEnvironment as string) - ? [...res[key].target] - : [...res[key].target, integration.targetEnvironment as string], + + ...(!isCustomEnvironment + ? { + target: res[key].target.includes(integration.targetEnvironment as string) + ? [...res[key].target] + : [...res[key].target, integration.targetEnvironment as string] + } + : { + customEnvironmentIds: res[key].customEnvironmentIds?.includes(integration.targetEnvironment as string) + ? [...(res[key].customEnvironmentIds || [])] + : [...(res[key]?.customEnvironmentIds || []), integration.targetEnvironment as string] + }), + ...(integration.path ? { gitBranch: integration.path diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index d997d52f4..67168f1dd 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -30,6 +30,7 @@ export enum SmtpTemplates { NewDeviceJoin = "newDevice.handlebars", OrgInvite = "organizationInvitation.handlebars", ResetPassword = "passwordReset.handlebars", + SetupPassword = "passwordSetup.handlebars", SecretLeakIncident = "secretLeakIncident.handlebars", WorkspaceInvite = "workspaceInvitation.handlebars", ScimUserProvisioned = "scimUserProvisioned.handlebars", diff --git a/backend/src/services/smtp/templates/passwordSetup.handlebars b/backend/src/services/smtp/templates/passwordSetup.handlebars new file mode 100644 index 000000000..1059a7446 --- /dev/null +++ b/backend/src/services/smtp/templates/passwordSetup.handlebars @@ -0,0 +1,17 @@ + + + + + Password Setup + + +

Setup your password

+

Someone requested to set up a password for your account.

+

Make sure you are already logged in to Infisical in the current browser before clicking the link below.

+ Setup password +

If you didn't initiate this request, please contact + {{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}

+ + {{emailFooter}} + + \ No newline at end of file diff --git a/company/documentation/engineering/how-to-write-design-doc.mdx b/company/documentation/engineering/how-to-write-design-doc.mdx new file mode 100644 index 000000000..7f6a49e47 --- /dev/null +++ b/company/documentation/engineering/how-to-write-design-doc.mdx @@ -0,0 +1,67 @@ +--- +title: "How to write a design document" +sidebarTitle: "Writing Design Docs" +description: "Learn how to write a design document at Infisical" +--- + +## **Why write a design document?** + +Writing a design document helps you efficiently solve broad, complex engineering problems at Infisical. While planning is important, we are a startup, so speed and urgency should be your top of mind. Keep the process lightweight and time boxed so that we can get the most out of it. + +**Writing a design will help you:** + +- **Understand the problem space:** Deeply understand the problem you’re solving to make sure it is well scoped. +- **Stay on the right path:** Without proper planning, you risk cycling between partial implementation and replanning, encountering roadblocks that force you back to square one. A solid plan minimizes wasted engineering hours. +- **An opportunity to collaborate:** Bring relevant engineers into the discussion to develop well-thought-out solutions and catch potential issues you might have overlooked. +- **Faster implementation:** A well-thought-out plan will help you catch roadblocks early and ship quickly because you know exactly what needs to get implemented. + +**When to write a design document:** + +- **Write a design doc**: If the feature is not well defined, high-security, or will take more than **1 full engineering week** to build. +- **Skip the design doc**: For small, straightforward features that can be built quickly with informal discussions. + +If you are unsure when to create a design doc, chat with @maidul. + +## **What to Include in your Design Document** + +Every feature/problem is unique, but your design docs should generally include the following sections. If you need to include additional sections, feel free to do so. + +1. **Title** + - A descriptive title. + - Name of document owner and name of reviewer(s). +2. **Overview** + - A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs). +3. **Context** + - Explain the problem’s background, why it’s important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.). +4. **Solution** + - Provide a big-picture explanation of the solution, followed by detailed technical architecture. + - Use diagrams/charts where needed. + - Write clearly so that another engineer could implement the solution in your absence. +5. **Milestones** + - Break the project into phases with clear start and end dates estimates. Use a table or bullet points. +6. **FAQ** + - Common questions or concerns someone might have while reading your document that can be quickly addressed. + + +## **How to Write a Design Doc** + +- **Keep it Simple**: Use clear, simple language. Opt for short sentences, bullet points, and concrete examples over fluff writing. +- **Use Visuals**: Add diagrams and charts for clarity to convey your ideas. +- **Make it Self-Explanatory**: Ensure that anyone reading the document can understand and implement the plan without needing additional context. + +Before sharing your design docs with others, review your design doc as if you were a teammate seeing it for the first time. Anticipate questions and address them. + + +## **Process from start to finish** + +1. **Research/Discuss** + - Before you start writing, take some time to research and get a solid understanding of the problem space. Look into how other well-established companies are tackling similar challenges, if they are. + Talk through the problem and your initial solution with other engineers on the team—bounce ideas around and get their feedback. If you have ideas on how the system could if implemented in Infisical, would it effect any downstream features/systems, etc? + + Once you’ve got a general direction, you might need to test a some theories. This is where quick proof of concepts (POCs) come in handy, but don’t get too caught up in the details. The goal of a POC is simply to validate a core idea or concept so you can get to the rest of your planning. +2. **Write the Doc** + - Based on your research/discussions, write the design doc and include all relevant sections. Your goal is to come up with a convincing plan on why this is the correct why to solve the problem at hand. +3. **Assign Reviewers** + - Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up. +4. **Team Review and Feedback** + - Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on. \ No newline at end of file diff --git a/company/mint.json b/company/mint.json index 11edf794e..247fb5a2b 100644 --- a/company/mint.json +++ b/company/mint.json @@ -66,7 +66,8 @@ { "group": "Engineering", "pages": [ - "documentation/engineering/oncall" + "documentation/engineering/oncall", + "documentation/engineering/how-to-write-design-doc" ] } ] diff --git a/docs/mint.json b/docs/mint.json index 7a0952c2c..31c5d7b14 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -344,34 +344,6 @@ "cli/faq" ] }, - { - "group": "App Connections", - "pages": [ - "integrations/app-connections/overview", - { - "group": "Connections", - "pages": [ - "integrations/app-connections/aws", - "integrations/app-connections/github", - "integrations/app-connections/gcp" - ] - } - ] - }, - { - "group": "Secret Syncs", - "pages": [ - "integrations/secret-syncs/overview", - { - "group": "Syncs", - "pages": [ - "integrations/secret-syncs/aws-parameter-store", - "integrations/secret-syncs/github", - "integrations/secret-syncs/gcp-secret-manager" - ] - } - ] - }, { "group": "Infrastructure Integrations", "pages": [ @@ -406,6 +378,34 @@ "integrations/platforms/ansible" ] }, + { + "group": "App Connections", + "pages": [ + "integrations/app-connections/overview", + { + "group": "Connections", + "pages": [ + "integrations/app-connections/aws", + "integrations/app-connections/github", + "integrations/app-connections/gcp" + ] + } + ] + }, + { + "group": "Secret Syncs", + "pages": [ + "integrations/secret-syncs/overview", + { + "group": "Syncs", + "pages": [ + "integrations/secret-syncs/aws-parameter-store", + "integrations/secret-syncs/github", + "integrations/secret-syncs/gcp-secret-manager" + ] + } + ] + }, { "group": "Native Integrations", "pages": [ diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 03e077a8d..42823af4d 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({ "/_restrict-login-signup/login/provider/success" ), SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"), - PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset") + PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"), + PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup") }, Organization: { SecretScanning: setRoute( diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 3a5070ef5..338671cab 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -10,6 +10,7 @@ export type TGetAuditLogsFilter = { actorType?: ActorType; projectId?: string; actor?: string; // user ID format + secretPath?: string; startDate?: Date; endDate?: Date; limit: number; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 8d8ee0c3f..9b8afbac7 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -23,6 +23,7 @@ import { MfaMethod, ResetPasswordDTO, SendMfaTokenDTO, + SetupPasswordDTO, SRP1DTO, SRPR1Res, TOauthTokenExchangeDTO, @@ -286,7 +287,8 @@ export const useResetPassword = () => { encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, salt: details.salt, - verifier: details.verifier + verifier: details.verifier, + password: details.password }, { headers: { @@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => { return data.isVerified; }; + +export const useSendPasswordSetupEmail = () => { + return useMutation({ + mutationFn: async () => { + const { data } = await apiRequest.post("/api/v1/password/email/password-setup"); + + return data; + } + }); +}; + +export const useSetupPassword = () => { + return useMutation({ + mutationFn: async (payload: SetupPasswordDTO) => { + const { data } = await apiRequest.post("/api/v1/password/password-setup", payload); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index d0c718e48..036897fed 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -133,6 +133,20 @@ export type ResetPasswordDTO = { salt: string; verifier: string; verificationToken: string; + password: string; +}; + +export type SetupPasswordDTO = { + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + token: string; + password: string; }; export type IssueBackupPrivateKeyDTO = { diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index e7ee5928a..eab946ba6 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -16,5 +16,6 @@ export { useGetIntegrationAuthTeamCityBuildConfigs, useGetIntegrationAuthTeams, useGetIntegrationAuthVercelBranches, + useGetIntegrationAuthVercelCustomEnvironments, useSaveIntegrationAccessToken } from "./queries"; diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 44b199a24..f62887043 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -21,7 +21,8 @@ import { Team, TeamCityBuildConfig, TGetIntegrationAuthOctopusDeployScopeValuesDTO, - TOctopusDeployVariableSetScopeValues + TOctopusDeployVariableSetScopeValues, + VercelEnvironment } from "./types"; const integrationAuthKeys = { @@ -132,7 +133,9 @@ const integrationAuthKeys = { }: TGetIntegrationAuthOctopusDeployScopeValuesDTO) => [{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const, getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) => - [{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const + [{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const, + getIntegrationAuthVercelCustomEnv: (integrationAuthId: string, teamId: string) => + [{ integrationAuthId, teamId }, "integrationAuthVercelCustomEnv"] as const }; const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -362,6 +365,29 @@ const fetchIntegrationAuthQoveryScopes = async ({ return undefined; }; +const fetchIntegrationAuthVercelCustomEnvironments = async ({ + integrationAuthId, + teamId +}: { + integrationAuthId: string; + teamId: string; +}) => { + const { + data: { environments } + } = await apiRequest.get<{ + environments: { + appId: string; + customEnvironments: VercelEnvironment[]; + }[]; + }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/custom-environments`, { + params: { + teamId + } + }); + + return environments; +}; + const fetchIntegrationAuthHerokuPipelines = async ({ integrationAuthId }: { @@ -730,6 +756,24 @@ export const useGetIntegrationAuthQoveryScopes = ({ }); }; +export const useGetIntegrationAuthVercelCustomEnvironments = ({ + integrationAuthId, + teamId +}: { + integrationAuthId: string; + teamId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthVercelCustomEnv(integrationAuthId, teamId), + queryFn: () => + fetchIntegrationAuthVercelCustomEnvironments({ + integrationAuthId, + teamId + }), + enabled: Boolean(teamId && integrationAuthId) + }); +}; + export const useGetIntegrationAuthHerokuPipelines = ({ integrationAuthId }: { diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index e2dee6067..b57e5aeca 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -43,6 +43,11 @@ export type Environment = { environmentId: string; }; +export type VercelEnvironment = { + id: string; + slug: string; +}; + export type ChecklyGroup = { name: string; groupId: number; diff --git a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx index ba28c3871..3361dd961 100644 --- a/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx +++ b/frontend/src/pages/auth/PasswordResetPage/PasswordResetPage.tsx @@ -136,7 +136,8 @@ export const PasswordResetPage = () => { encryptedPrivateKeyTag, salt: result.salt, verifier: result.verifier, - verificationToken + verificationToken, + password: newPassword }); navigate({ to: "/login" }); diff --git a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx new file mode 100644 index 000000000..311d613ef --- /dev/null +++ b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx @@ -0,0 +1,349 @@ +import crypto from "crypto"; + +import { FormEvent, useState } from "react"; +import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useSearch } from "@tanstack/react-router"; +import jsrp from "jsrp"; + +import { createNotification } from "@app/components/notifications"; +import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; +import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; +import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; +import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { useSetupPassword } from "@app/hooks/api/auth/queries"; + +// eslint-disable-next-line new-cap +const client = new jsrp.client(); + +export const PasswordSetupPage = () => { + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [passwordsMatch, setPasswordsMatch] = useState(true); + const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(true); + const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); + const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(true); + const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(true); + const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false); + const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false); + const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false); + const [passwordErrorBreached, setPasswordErrorBreached] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); + + const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordSetupPage.id }); + + const navigate = useNavigate(); + + const setupPassword = useSetupPassword(); + + const parsedUrl = search; + const token = parsedUrl.token as string; + const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); + + const handleSetPassword = async (e: FormEvent) => { + e.preventDefault(); + const errorCheck = await passwordCheck({ + password, + setPasswordErrorTooShort, + setPasswordErrorTooLong, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached + }); + + if (password !== confirmPassword) { + setPasswordsMatch(false); + return; + } + + setPasswordsMatch(true); + + if (!errorCheck) { + client.init( + { + username: email, + password + }, + async () => { + client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { + const derivedKey = await deriveArgonKey({ + password, + salt: result.salt, + mem: 65536, + time: 3, + parallelism: 1, + hashLen: 32 + }); + + if (!derivedKey) throw new Error("Failed to derive key from password"); + + const key = crypto.randomBytes(32); + + // create encrypted private key by encrypting the private + // key with the symmetric key [key] + const { + ciphertext: encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag + } = Aes256Gcm.encrypt({ + text: localStorage.getItem("PRIVATE_KEY") as string, + secret: key + }); + + // create the protected key by encrypting the symmetric key + // [key] with the derived key + const { + ciphertext: protectedKey, + iv: protectedKeyIV, + tag: protectedKeyTag + } = Aes256Gcm.encrypt({ + text: key.toString("hex"), + secret: Buffer.from(derivedKey.hash) + }); + + try { + await setupPassword.mutateAsync({ + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt: result.salt, + verifier: result.verifier, + token, + password + }); + + setIsRedirecting(true); + + createNotification({ + type: "success", + title: "Password successfully set", + text: "Redirecting to login..." + }); + + setTimeout(() => { + window.location.href = "/login"; + }, 3000); + } catch (error) { + createNotification({ + type: "error", + text: (error as Error).message ?? "Error setting password" + }); + navigate({ to: "/personal-settings" }); + } + }); + } + ); + } + }; + + const isInvalidPassword = + passwordErrorTooShort || + passwordErrorTooLong || + passwordErrorNoLetterChar || + passwordErrorNoNumOrSpecialChar || + passwordErrorRepeatedChar || + passwordErrorEscapeChar || + passwordErrorLowEntropy || + passwordErrorBreached; + + return ( +
+
+ + +
+
+ +
+ Set Password +
+
+ + { + setPassword(e.target.value); + passwordCheck({ + password: e.target.value, + setPasswordErrorTooShort, + setPasswordErrorTooLong, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached + }); + }} + rightIcon={ + + } + /> + + + setConfirmPassword(e.target.value)} + rightIcon={ + + } + /> + +
+
Password must contain:
+
+ {passwordErrorTooShort ? ( + + ) : ( + + )} +
+ at least 14 characters +
+
+
+ {passwordErrorTooLong ? ( + + ) : ( + + )} +
+ at most 100 characters +
+
+
+ {passwordErrorNoLetterChar ? ( + + ) : ( + + )} +
+ at least 1 letter character +
+
+
+ {passwordErrorNoNumOrSpecialChar ? ( + + ) : ( + + )} +
+ at least 1 number or special character +
+
+
+ {passwordErrorRepeatedChar ? ( + + ) : ( + + )} +
+ at most 3 repeated, consecutive characters +
+
+
+ {passwordErrorEscapeChar ? ( + + ) : ( + + )} +
+ no escape characters +
+
+
+ {passwordErrorLowEntropy ? ( + + ) : ( + + )} +
+ no personal information +
+
+
+ {passwordErrorBreached ? ( + + ) : ( + + )} +
+ password not found in a data breach. +
+
+
+ +
+
+
+ ); +}; diff --git a/frontend/src/pages/auth/PasswordSetupPage/route.tsx b/frontend/src/pages/auth/PasswordSetupPage/route.tsx new file mode 100644 index 000000000..5224feedb --- /dev/null +++ b/frontend/src/pages/auth/PasswordSetupPage/route.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { PasswordSetupPage } from "./PasswordSetupPage"; + +const PasswordSetupPageQueryParamsSchema = z.object({ + token: z.string(), + to: z.string() +}); + +export const Route = createFileRoute("/_authenticate/password-setup")({ + component: PasswordSetupPage, + validateSearch: zodValidator(PasswordSetupPageQueryParamsSchema) +}); diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 7810690e5..cb9690837 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -1,12 +1,13 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; +import { ROUTE_PATHS } from "@app/const/routes"; import { userKeys } from "@app/hooks/api"; import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; import { fetchUserDetails } from "@app/hooks/api/users/queries"; export const Route = createFileRoute("/_authenticate")({ - beforeLoad: async ({ context }) => { + beforeLoad: async ({ context, location }) => { if (!context.serverConfig.initialized) { throw redirect({ to: "/admin/signup" }); } @@ -26,7 +27,7 @@ export const Route = createFileRoute("/_authenticate")({ }); }); - if (!data.organizationId) { + if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path) { throw redirect({ to: "/login/select-organization" }); } diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 3a21958ae..5c7e020b7 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -14,6 +14,7 @@ import { DropdownMenuTrigger, FilterableSelect, FormControl, + Input, Select, SelectItem } from "@app/components/v2"; @@ -22,6 +23,7 @@ import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/ import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; import { Actor } from "@app/hooks/api/auditLogs/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { AuditLogFilterFormData } from "./types"; @@ -50,6 +52,7 @@ export const LogsFilter = ({ className, control, reset, + setValue, watch }: Props) => { const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); @@ -101,6 +104,7 @@ export const LogsFilter = ({ }; const selectedEventTypes = watch("eventType") as EventType[] | undefined; + const selectedProject = watch("project"); return (
- {isOrgAuditLogs && workspacesInOrg.length > 0 && ( - ( - - ({ name, id }))} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - - )} - /> - )} +
+ {isOrgAuditLogs && workspacesInOrg.length > 0 && ( + ( + + { + if (e === null) { + setValue("secretPath", ""); + } + onChange(e); + }} + placeholder="Select a project..." + options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + + )} + /> + )} + {selectedProject?.type === ProjectType.SecretManager && ( + ( + + onChange(e.target.value)} /> + + )} + /> + )} +
(secretPath!, 500); + return (
{showFilters && ( @@ -90,6 +94,7 @@ export const LogsSection = withPermission( isOrgAuditLogs={isOrgAuditLogs} showActorColumn={!!showActorColumn} filter={{ + secretPath: debouncedSecretPath || undefined, eventMetadata: presets?.eventMetadata, projectId, actorType: presets?.actorType, diff --git a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx index 05c44fa4d..17dcb80fd 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx @@ -1,14 +1,19 @@ import { z } from "zod"; import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; +import { ProjectType } from "@app/hooks/api/workspace/types"; export const auditLogFilterFormSchema = z .object({ eventMetadata: z.object({}).optional(), - project: z.object({ id: z.string(), name: z.string() }).optional().nullable(), + project: z + .object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) }) + .optional() + .nullable(), eventType: z.nativeEnum(EventType).array(), actor: z.string().optional(), userAgentType: z.nativeEnum(UserAgentType), + secretPath: z.string().optional(), startDate: z.date().optional(), endDate: z.date().optional(), page: z.coerce.number().optional(), diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx index aec636da7..241e673ca 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/IntegrationsListPage.tsx @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { IntegrationsListPageTabs } from "@app/types/integrations"; @@ -45,14 +45,12 @@ export const IntegrationsListPage = () => {
-
-
-

Integrations

-

- Manage integrations with third-party services. -

-
-
+
+ +
Integrations Update diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx index 1a67e98a0..db3787b49 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/SecretSyncDetailsByIDPage.tsx @@ -73,7 +73,7 @@ const PageContent = () => { return ( <>
-
+
diff --git a/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx index 5581cca4a..e441b99b3 100644 --- a/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AzureDevopsAuthorizePage/AzureDevopsAuthorizePage.tsx @@ -65,7 +65,7 @@ export const AzureDevopsAuthorizePage = () => {
Azure DevOps logo {
Azure DevOps logo
- Github logo + Azure Key Vault logo
Azure Key Vault Integration { teamId: integrationAuth?.teamId as string }); + const { data: customEnvironments } = useGetIntegrationAuthVercelCustomEnvironments({ + teamId: integrationAuth?.teamId as string, + integrationAuthId: integrationAuthId as string + }); + const { data: branches } = useGetIntegrationAuthVercelBranches({ integrationAuthId: integrationAuthId as string, appId: targetAppId @@ -135,6 +141,26 @@ export const VercelConfigurePage = () => { } }; + const selectedVercelEnvironments = useMemo(() => { + let selectedEnvironments = vercelEnvironments; + + const environments = customEnvironments?.find( + (e) => e.appId === targetAppId + )?.customEnvironments; + + if (environments && environments.length > 0) { + selectedEnvironments = [ + ...selectedEnvironments, + ...environments.map((env) => ({ + name: env.slug, + slug: env.id + })) + ]; + } + + return selectedEnvironments; + }, [targetAppId, customEnvironments]); + return integrationAuth && selectedSourceEnvironment && integrationAuthApps && @@ -210,7 +236,13 @@ export const VercelConfigurePage = () => { >