Compare commits

...

87 Commits

Author SHA1 Message Date
0191eb48f3 Merge pull request #3974 from Infisical/fix-email-invite-notifications
Improve + fix invitation reminder logic
2025-07-07 14:47:50 -04:00
9d39910152 Minor fix to prevent setting lastInvitedAt for invitees who weren’t actually sent an invitation 2025-07-07 15:35:49 -03:00
9137fa4ca5 Improve + fix invitation reminder logic 2025-07-07 13:31:20 -04:00
78da7ec343 Merge pull request #3972 from Infisical/fix/telemetryOrgIdentify
feat(telemetry): improve Posthog org identity logic
2025-07-07 10:15:59 -03:00
a678ebb4ac Fix Cloud telemetry queue initialization 2025-07-07 10:10:30 -03:00
83dd38db49 feat(telemetry): reduce TELEMETRY_AGGREGATED_KEY_EXP to 10 mins and avoid sending org identitfy events for batch events on sendPostHogEvents 2025-07-07 08:36:15 -03:00
06f5af1200 Merge pull request #3890 from Infisical/daniel/sso-endpoints-docs
docs(api-reference/organizations): document SSO configuration endpoints
2025-07-04 05:33:52 +04:00
f903e5b3d4 Update saml-router.ts 2025-07-04 05:23:05 +04:00
c6f8915d3f Update saml-config-service.ts 2025-07-04 05:21:54 +04:00
65b1354ef1 fix: remove undefined return type from get saml endpoint 2025-07-04 05:07:54 +04:00
cda8579ca4 fix: requested changes 2025-07-04 04:51:14 +04:00
1b1acdcb0b Merge pull request #3917 from Infisical/cli-add-bitbucket-platform
Add BitBucket platform to secret scanning
2025-07-03 20:06:48 -04:00
a8f08730a1 Merge pull request #3908 from Infisical/fix/ui-small-catches
feat: added autoplay to loading lottie and fixed tooltip in project select
2025-07-03 19:35:59 -04:00
9af9050aa2 Merge pull request #3921 from Infisical/misc/allow-users-with-create-identity-to-invite-no-access
misc: allow users with create permission to add identities with no access
2025-07-03 19:27:04 -04:00
cc564119e0 misc: allow users with create permission to add identities with no access 2025-07-04 04:24:15 +08:00
189b0dd5ee Merge pull request #3920 from Infisical/fix-secret-sync-remove-and-import-audit-logs
fix(secret-syncs): pass audit log info from import/delete secrets for sync endpoint
2025-07-03 13:02:04 -07:00
9cbef2c07b fix: pass audit log info from import/delete secrets for sync endpoint 2025-07-03 12:37:28 -07:00
9a960a85cd Merge pull request #3905 from Infisical/password-reset-ui
improvement(password-reset): re-vamp password reset flow pages/steps to match login
2025-07-03 10:31:58 -07:00
2a9e31d305 Few nits 2025-07-03 13:11:53 -04:00
fb2f1731dd Merge branch 'main' into password-reset-ui 2025-07-03 13:02:48 -04:00
42648a134c Update utils.go to look more like Gitleaks version 2025-07-03 12:47:25 -04:00
defb66ce65 Merge pull request #3918 from Infisical/revert-3901-revert-3875-ENG-3009-test
Undo Environment Variables Override PR Revert + SSO Fix
2025-07-03 12:18:10 -04:00
a3d06fdf1b misc: added reference to server admin 2025-07-03 21:21:06 +08:00
9049c441d6 Greptile review fix 2025-07-03 03:18:37 -04:00
51ecc9dfa0 Merge branch 'revert-3899-revert-3896-misc/final-changes-for-self-serve-en' into revert-3901-revert-3875-ENG-3009-test 2025-07-03 03:08:42 -04:00
13c9879fb6 Merge branch 'main' into revert-3901-revert-3875-ENG-3009-test 2025-07-03 02:54:28 -04:00
23b20ebdab Fix CLI always defaulting to github 2025-07-03 00:49:31 -04:00
37d490ede3 Add BitBucket platform to secret scanning 2025-07-03 00:09:28 -04:00
73025f5094 Merge pull request #3916 from Infisical/revert-3915-revert-3914-daniel/infisical-helm
Revert "Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support""
2025-07-03 05:25:24 +04:00
82634983ce Update Chart.yaml 2025-07-03 05:19:30 +04:00
af2f3017b7 fix: tests failing 2025-07-03 05:13:50 +04:00
a8f0eceeb9 Update helm-release-infisical-core.yml 2025-07-03 05:00:51 +04:00
36ff5e054b Update helm-release-infisical-core.yml 2025-07-03 04:50:49 +04:00
eff73f1810 fix: update versions 2025-07-03 04:27:55 +04:00
68357b5669 Revert "Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support"" 2025-07-02 20:25:36 -04:00
03c2e93bea Merge pull request #3915 from Infisical/revert-3914-daniel/infisical-helm
Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support"
2025-07-02 20:25:33 -04:00
8c1f3837e7 Revert "feat(helm-charts/infiscal-core): topologySpreadConstraints support" 2025-07-03 04:24:40 +04:00
7b47d91cc1 Merge pull request #3914 from Infisical/daniel/infisical-helm
feat(helm-charts/infiscal-core): topologySpreadConstraints support
2025-07-03 04:21:34 +04:00
c37afaa050 feat(helm-charts/infiscal-core): topologySpreadConstraints support 2025-07-03 04:08:37 +04:00
811920f8bb Merge pull request #3870 from Infisical/feat/zabbixSyncIntegration
feat(secret-sync): add Zabbix secret sync
2025-07-02 20:59:51 -03:00
7b295c5a21 Merge pull request #3913 from Infisical/daniel/fix-folder-deletion
fix(secret-folders): delete folder by ID
2025-07-03 03:49:01 +04:00
527a727c1c fix: ts issue 2025-07-03 03:28:21 +04:00
0139064aaa Update secret-folder-service.ts 2025-07-03 03:17:10 +04:00
a3859170fe fix(secret-folders): delete folder by ID 2025-07-03 03:15:06 +04:00
02b97cbf5b Merge pull request #3912 from Infisical/fix/multiEnvDeleteErrorMessage
Improve multi-env error message to show full env name instead of slug
2025-07-02 17:43:32 -04:00
8a65343f79 Add 15 seconds default duration for toast notifications 2025-07-02 18:42:02 -03:00
cf6181eb73 Improve multi-env error message to show full env name instead of slug 2025-07-02 18:25:49 -03:00
984ffd2a53 Merge pull request #3911 from Infisical/fix/policyFolderDeletionAndBatchMessage
Fix root folder issue with folder policies check and multi env error message improvement
2025-07-02 17:46:18 -03:00
a1c44bd7a2 Improve multi-env error message 2025-07-02 17:40:37 -03:00
d7860e2491 Merge pull request #3904 from Infisical/secret-overview-expandable-header
improvement: allow users to expand collapsed environment view header
2025-07-02 12:51:02 -07:00
db33349f49 Merge pull request #3910 from Infisical/misc/updated-worker-count-for-secret-scanning-jobs
misc: downsize worker count for secret scanning jobs
2025-07-02 12:50:37 -07:00
=
7ab67db84d feat: fixed black color in tooltip 2025-07-03 01:18:52 +05:30
e14bb6b901 Fix root folder issue with folder policies check and multi env error message improvement 2025-07-02 16:22:16 -03:00
=
3a17281e37 feat: resolved tooltip overflow 2025-07-03 00:41:47 +05:30
91d6d5d07b misc: updated worker count for secret scanning jobs 2025-07-03 03:02:16 +08:00
ac7b23da45 Merge pull request #3909 from Infisical/misc/update-tooltip-for-overwrite-sync
misc: update tooltip for overwrite sync
2025-07-03 02:57:52 +08:00
1fdc82e494 misc: update tooltip for overwrite sync 2025-07-03 02:32:10 +08:00
3daae6f965 improvement: adjust header drag to use table container for positioning 2025-07-02 11:10:37 -07:00
833963af0c improvement: remove additional relative and adjust handle position 2025-07-02 11:01:51 -07:00
aa560b8199 improvement: address feedback 2025-07-02 10:57:14 -07:00
a215b99b3c Merge pull request #3906 from Infisical/feat/audit-log-fix
feat: audit log improvement
2025-07-03 01:49:06 +08:00
=
fbd9ecd980 feat: fixed ts error 2025-07-02 23:04:36 +05:30
=
3b839d4826 feat: addressed review comments 2025-07-02 23:04:36 +05:30
=
b52ec37f76 feat: added query size validation for audit log 2025-07-02 23:04:36 +05:30
=
5709afe0d3 feat: lint errors fix 2025-07-02 23:04:36 +05:30
=
566a243520 feat: seperated date filter 2025-07-02 23:04:36 +05:30
=
147c21ab9f feat: updated backend logic to use parition and speed up audit log queries 2025-07-02 23:04:36 +05:30
=
abfe185a5b feat: added autoplay to loading lottie and fixed tooltip in project select 2025-07-02 22:13:37 +05:30
f62eb9f8a2 Merge pull request #3892 from Infisical/ENG-1946
feat: Re-invite users every 1 week for up to a month.
2025-07-02 12:08:13 -04:00
ec60080e27 Merge pull request #3907 from Infisical/misc/update-cli-releaser-spec
misc: updated CLI releaser spec
2025-07-02 10:44:55 -04:00
9fdc56bd6c misc: updated CLI releaser spec 2025-07-02 22:41:51 +08:00
f6c10683a5 misc: add sync for passport middleware 2025-07-02 20:48:24 +08:00
bb59bb1868 Remove file 2025-07-01 22:46:16 -04:00
139f880be1 merge 2025-07-01 22:43:20 -04:00
69157cb912 improvement: add period 2025-07-01 19:23:13 -07:00
44eb761d5b improvement: re-vamp password reset flow pages/steps to match login design 2025-07-01 19:19:27 -07:00
1a7b810bad improvement: allow users to expand collapsed environment view header 2025-07-01 17:22:49 -07:00
c2ce1aa5aa Fix license fns 2025-07-01 20:06:51 -04:00
c8e155f0ca Review fixes 2025-07-01 19:48:17 -04:00
abbf541c9f Docs link on UI 2025-07-01 19:01:39 -04:00
fcdd121a58 Docs & UI update 2025-07-01 18:46:06 -04:00
5bfd92bf8d Revert "Revert "feat(super-admin): Environment Overrides"" 2025-07-01 17:43:52 -04:00
45af2c0b49 Revert "Revert "misc: updated sidebar name"" 2025-07-01 17:42:54 -04:00
e5bbc46b0f Add org caching + fix a line 2025-07-01 00:07:10 -04:00
60a4c72a5d feat: Re-invite users every 1 week for up to a month. 2025-06-30 20:10:30 -04:00
13d2cbd8b0 Update docs.json 2025-07-01 02:09:14 +04:00
abfc5736fd docs(api-reference/organizations): document SSO configuration endpoints 2025-07-01 02:05:53 +04:00
103 changed files with 2697 additions and 1080 deletions

View File

@ -83,7 +83,7 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
goreleaser:
runs-on: ubuntu-latest
runs-on: ubuntu-latest-8-cores
needs: [cli-integration-tests]
steps:
- uses: actions/checkout@v3

View File

@ -110,7 +110,8 @@ export const initAuditLogDbConnection = ({
},
migrations: {
tableName: "infisical_migrations"
}
},
pool: { min: 0, max: 10 }
});
// we add these overrides so that auditLogDb and the primary DB are interchangeable

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (!hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.binary("encryptedEnvOverrides").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedEnvOverrides");
if (hasColumn) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("encryptedEnvOverrides");
});
}
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
if (!hasColumn) {
t.datetime("lastInvitedAt").nullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
if (hasColumn) {
t.dropColumn("lastInvitedAt");
}
});
}

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
if (hasColumn) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.datetime("lastInvitedAt").nullable().defaultTo(knex.fn.now()).alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
if (hasColumn) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.datetime("lastInvitedAt").nullable().alter();
});
}
}

View File

@ -18,7 +18,8 @@ export const OrgMembershipsSchema = z.object({
orgId: z.string().uuid(),
roleId: z.string().uuid().nullable().optional(),
projectFavorites: z.string().array().nullable().optional(),
isActive: z.boolean().default(true)
isActive: z.boolean().default(true),
lastInvitedAt: z.date().nullable().optional()
});
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;

View File

@ -34,7 +34,8 @@ export const SuperAdminSchema = z.object({
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional()
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional(),
encryptedEnvOverrides: zodBuffer.nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@ -17,6 +17,7 @@ import { z } from "zod";
import { LdapGroupMapsSchema } from "@app/db/schemas";
import { TLDAPConfig } from "@app/ee/services/ldap-config/ldap-config-types";
import { isValidLdapFilter, searchGroups } from "@app/ee/services/ldap-config/ldap-fns";
import { ApiDocsTags, LdapSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
@ -132,10 +133,18 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Get LDAP config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
organizationId: z.string().trim()
organizationId: z.string().trim().describe(LdapSso.GET_CONFIG.organizationId)
}),
response: {
200: z.object({
@ -172,23 +181,32 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Create LDAP config",
security: [
{
bearerAuth: []
}
],
body: z.object({
organizationId: z.string().trim(),
isActive: z.boolean(),
url: z.string().trim(),
bindDN: z.string().trim(),
bindPass: z.string().trim(),
uniqueUserAttribute: z.string().trim().default("uidNumber"),
searchBase: z.string().trim(),
searchFilter: z.string().trim().default("(uid={{username}})"),
groupSearchBase: z.string().trim(),
organizationId: z.string().trim().describe(LdapSso.CREATE_CONFIG.organizationId),
isActive: z.boolean().describe(LdapSso.CREATE_CONFIG.isActive),
url: z.string().trim().describe(LdapSso.CREATE_CONFIG.url),
bindDN: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindDN),
bindPass: z.string().trim().describe(LdapSso.CREATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim().default("uidNumber").describe(LdapSso.CREATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.searchBase),
searchFilter: z.string().trim().default("(uid={{username}})").describe(LdapSso.CREATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim().describe(LdapSso.CREATE_CONFIG.groupSearchBase),
groupSearchFilter: z
.string()
.trim()
.default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"),
caCert: z.string().trim().default("")
.default("(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))")
.describe(LdapSso.CREATE_CONFIG.groupSearchFilter),
caCert: z.string().trim().default("").describe(LdapSso.CREATE_CONFIG.caCert)
}),
response: {
200: SanitizedLdapConfigSchema
@ -214,23 +232,31 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.LdapSso],
description: "Update LDAP config",
security: [
{
bearerAuth: []
}
],
body: z
.object({
isActive: z.boolean(),
url: z.string().trim(),
bindDN: z.string().trim(),
bindPass: z.string().trim(),
uniqueUserAttribute: z.string().trim(),
searchBase: z.string().trim(),
searchFilter: z.string().trim(),
groupSearchBase: z.string().trim(),
groupSearchFilter: z.string().trim(),
caCert: z.string().trim()
isActive: z.boolean().describe(LdapSso.UPDATE_CONFIG.isActive),
url: z.string().trim().describe(LdapSso.UPDATE_CONFIG.url),
bindDN: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindDN),
bindPass: z.string().trim().describe(LdapSso.UPDATE_CONFIG.bindPass),
uniqueUserAttribute: z.string().trim().describe(LdapSso.UPDATE_CONFIG.uniqueUserAttribute),
searchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchBase),
searchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.searchFilter),
groupSearchBase: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchBase),
groupSearchFilter: z.string().trim().describe(LdapSso.UPDATE_CONFIG.groupSearchFilter),
caCert: z.string().trim().describe(LdapSso.UPDATE_CONFIG.caCert)
})
.partial()
.merge(z.object({ organizationId: z.string() })),
.merge(z.object({ organizationId: z.string().trim().describe(LdapSso.UPDATE_CONFIG.organizationId) })),
response: {
200: SanitizedLdapConfigSchema
}

View File

@ -13,6 +13,7 @@ import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas";
import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
import { ApiDocsTags, OidcSSo } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -153,10 +154,18 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Get OIDC config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
orgSlug: z.string().trim()
organizationId: z.string().trim().describe(OidcSSo.GET_CONFIG.organizationId)
}),
response: {
200: SanitizedOidcConfigSchema.pick({
@ -180,9 +189,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { orgSlug } = req.query;
const oidc = await server.services.oidc.getOidc({
orgSlug,
organizationId: req.query.organizationId,
type: "external",
actor: req.permission.type,
actorId: req.permission.id,
@ -200,8 +208,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Update OIDC config",
security: [
{
bearerAuth: []
}
],
body: z
.object({
allowedEmailDomains: z
@ -216,22 +232,26 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",")
.map((id) => id.trim())
.join(", ");
}),
discoveryURL: z.string().trim(),
configurationType: z.nativeEnum(OIDCConfigurationType),
issuer: z.string().trim(),
authorizationEndpoint: z.string().trim(),
jwksUri: z.string().trim(),
tokenEndpoint: z.string().trim(),
userinfoEndpoint: z.string().trim(),
clientId: z.string().trim(),
clientSecret: z.string().trim(),
isActive: z.boolean(),
manageGroupMemberships: z.boolean().optional(),
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional()
})
.describe(OidcSSo.UPDATE_CONFIG.allowedEmailDomains),
discoveryURL: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.discoveryURL),
configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.UPDATE_CONFIG.configurationType),
issuer: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.issuer),
authorizationEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.authorizationEndpoint),
jwksUri: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.jwksUri),
tokenEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.tokenEndpoint),
userinfoEndpoint: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.userinfoEndpoint),
clientId: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientId),
clientSecret: z.string().trim().describe(OidcSSo.UPDATE_CONFIG.clientSecret),
isActive: z.boolean().describe(OidcSSo.UPDATE_CONFIG.isActive),
manageGroupMemberships: z.boolean().optional().describe(OidcSSo.UPDATE_CONFIG.manageGroupMemberships),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.describe(OidcSSo.UPDATE_CONFIG.jwtSignatureAlgorithm)
})
.partial()
.merge(z.object({ orgSlug: z.string() })),
.merge(z.object({ organizationId: z.string().describe(OidcSSo.UPDATE_CONFIG.organizationId) })),
response: {
200: SanitizedOidcConfigSchema.pick({
id: true,
@ -267,8 +287,16 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OidcSso],
description: "Create OIDC config",
security: [
{
bearerAuth: []
}
],
body: z
.object({
allowedEmailDomains: z
@ -283,23 +311,34 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
.split(",")
.map((id) => id.trim())
.join(", ");
}),
configurationType: z.nativeEnum(OIDCConfigurationType),
issuer: z.string().trim().optional().default(""),
discoveryURL: z.string().trim().optional().default(""),
authorizationEndpoint: z.string().trim().optional().default(""),
jwksUri: z.string().trim().optional().default(""),
tokenEndpoint: z.string().trim().optional().default(""),
userinfoEndpoint: z.string().trim().optional().default(""),
clientId: z.string().trim(),
clientSecret: z.string().trim(),
isActive: z.boolean(),
orgSlug: z.string().trim(),
manageGroupMemberships: z.boolean().optional().default(false),
})
.describe(OidcSSo.CREATE_CONFIG.allowedEmailDomains),
configurationType: z.nativeEnum(OIDCConfigurationType).describe(OidcSSo.CREATE_CONFIG.configurationType),
issuer: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.issuer),
discoveryURL: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.discoveryURL),
authorizationEndpoint: z
.string()
.trim()
.optional()
.default("")
.describe(OidcSSo.CREATE_CONFIG.authorizationEndpoint),
jwksUri: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.jwksUri),
tokenEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.tokenEndpoint),
userinfoEndpoint: z.string().trim().optional().default("").describe(OidcSSo.CREATE_CONFIG.userinfoEndpoint),
clientId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientId),
clientSecret: z.string().trim().describe(OidcSSo.CREATE_CONFIG.clientSecret),
isActive: z.boolean().describe(OidcSSo.CREATE_CONFIG.isActive),
organizationId: z.string().trim().describe(OidcSSo.CREATE_CONFIG.organizationId),
manageGroupMemberships: z
.boolean()
.optional()
.default(false)
.describe(OidcSSo.CREATE_CONFIG.manageGroupMemberships),
jwtSignatureAlgorithm: z
.nativeEnum(OIDCJWTSignatureAlgorithm)
.optional()
.default(OIDCJWTSignatureAlgorithm.RS256)
.describe(OidcSSo.CREATE_CONFIG.jwtSignatureAlgorithm)
})
.superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) {

View File

@ -111,15 +111,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
params: z.object({
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
}),
querystring: z.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
}),
querystring: z
.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
})
.superRefine((el, ctx) => {
if (el.endDate && el.startDate) {
const startDate = new Date(el.startDate);
const endDate = new Date(el.endDate);
const maxAllowedDate = new Date(startDate);
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
if (endDate < startDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "End date cannot be before start date"
});
}
if (endDate > maxAllowedDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "Dates must be within 3 months"
});
}
}
}),
response: {
200: z.object({
auditLogs: AuditLogsSchema.omit({
@ -161,7 +184,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
filter: {
...req.query,
projectId: req.params.workspaceId,
endDate: req.query.endDate,
endDate: req.query.endDate || new Date().toISOString(),
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined

View File

@ -13,6 +13,7 @@ import { FastifyRequest } from "fastify";
import { z } from "zod";
import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types";
import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
@ -149,8 +150,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
firstName,
lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
metadata: userMetadata
});
cb(null, { isUserCompleted, providerAuthToken });
@ -262,25 +263,31 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Get SAML config",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
organizationId: z.string().trim()
organizationId: z.string().trim().describe(SamlSso.GET_CONFIG.organizationId)
}),
response: {
200: z
.object({
id: z.string(),
organization: z.string(),
orgId: z.string(),
authProvider: z.string(),
isActive: z.boolean(),
entryPoint: z.string(),
issuer: z.string(),
cert: z.string(),
lastUsed: z.date().nullable().optional()
})
.optional()
200: z.object({
id: z.string(),
organization: z.string(),
orgId: z.string(),
authProvider: z.string(),
isActive: z.boolean(),
entryPoint: z.string(),
issuer: z.string(),
cert: z.string(),
lastUsed: z.date().nullable().optional()
})
}
},
handler: async (req) => {
@ -302,15 +309,23 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Create SAML config",
security: [
{
bearerAuth: []
}
],
body: z.object({
organizationId: z.string(),
authProvider: z.nativeEnum(SamlProviders),
isActive: z.boolean(),
entryPoint: z.string(),
issuer: z.string(),
cert: z.string()
organizationId: z.string().trim().describe(SamlSso.CREATE_CONFIG.organizationId),
authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.CREATE_CONFIG.authProvider),
isActive: z.boolean().describe(SamlSso.CREATE_CONFIG.isActive),
entryPoint: z.string().trim().describe(SamlSso.CREATE_CONFIG.entryPoint),
issuer: z.string().trim().describe(SamlSso.CREATE_CONFIG.issuer),
cert: z.string().trim().describe(SamlSso.CREATE_CONFIG.cert)
}),
response: {
200: SanitizedSamlConfigSchema
@ -341,18 +356,26 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.SamlSso],
description: "Update SAML config",
security: [
{
bearerAuth: []
}
],
body: z
.object({
authProvider: z.nativeEnum(SamlProviders),
isActive: z.boolean(),
entryPoint: z.string(),
issuer: z.string(),
cert: z.string()
authProvider: z.nativeEnum(SamlProviders).describe(SamlSso.UPDATE_CONFIG.authProvider),
isActive: z.boolean().describe(SamlSso.UPDATE_CONFIG.isActive),
entryPoint: z.string().trim().describe(SamlSso.UPDATE_CONFIG.entryPoint),
issuer: z.string().trim().describe(SamlSso.UPDATE_CONFIG.issuer),
cert: z.string().trim().describe(SamlSso.UPDATE_CONFIG.cert)
})
.partial()
.merge(z.object({ organizationId: z.string() })),
.merge(z.object({ organizationId: z.string().trim().describe(SamlSso.UPDATE_CONFIG.organizationId) })),
response: {
200: SanitizedSamlConfigSchema
}

View File

@ -30,10 +30,10 @@ type TFindQuery = {
actor?: string;
projectId?: string;
environment?: string;
orgId?: string;
orgId: string;
eventType?: string;
startDate?: string;
endDate?: string;
startDate: string;
endDate: string;
userAgentType?: string;
limit?: number;
offset?: number;
@ -61,18 +61,15 @@ export const auditLogDALFactory = (db: TDbClient) => {
},
tx
) => {
if (!orgId && !projectId) {
throw new Error("Either orgId or projectId must be provided");
}
try {
// Find statements
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
.where(`${TableName.AuditLog}.orgId`, orgId)
.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate])
.andWhereRaw(`"${TableName.AuditLog}"."createdAt" < ?::timestamptz`, [endDate])
// eslint-disable-next-line func-names
.where(function () {
if (orgId) {
void this.where(`${TableName.AuditLog}.orgId`, orgId);
} else if (projectId) {
if (projectId) {
void this.where(`${TableName.AuditLog}.projectId`, projectId);
}
});
@ -135,14 +132,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
void sqlQuery.whereIn("eventType", eventType);
}
// Filter by date range
if (startDate) {
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" >= ?::timestamptz`, [startDate]);
}
if (endDate) {
void sqlQuery.whereRaw(`"${TableName.AuditLog}"."createdAt" <= ?::timestamptz`, [endDate]);
}
// we timeout long running queries to prevent DB resource issues (2 minutes)
const docs = await sqlQuery.timeout(1000 * 120);
@ -174,6 +163,8 @@ export const auditLogDALFactory = (db: TDbClient) => {
try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
.where("expiresAt", "<", today)
.where("createdAt", "<", today) // to use audit log partition
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
.select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);

View File

@ -67,7 +67,8 @@ export const auditLogServiceFactory = ({
secretPath: filter.secretPath,
secretKey: filter.secretKey,
environment: filter.environment,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
orgId: actorOrgId,
...(filter.projectId ? { projectId: filter.projectId } : {})
});
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({

View File

@ -56,8 +56,8 @@ export type TListProjectAuditLogDTO = {
eventType?: EventType[];
offset?: number;
limit: number;
endDate?: string;
startDate?: string;
endDate: string;
startDate: string;
projectId?: string;
environment?: string;
auditLogActorId?: string;

View File

@ -107,34 +107,26 @@ export const oidcConfigServiceFactory = ({
kmsService
}: TOidcConfigServiceFactoryDep) => {
const getOidc = async (dto: TGetOidcCfgDTO) => {
const org = await orgDAL.findOne({ slug: dto.orgSlug });
if (!org) {
const oidcCfg = await oidcConfigDAL.findOne({
orgId: dto.organizationId
});
if (!oidcCfg) {
throw new NotFoundError({
message: `Organization with slug '${dto.orgSlug}' not found`,
name: "OrgNotFound"
message: `OIDC configuration for organization with ID '${dto.organizationId}' not found`
});
}
if (dto.type === "external") {
const { permission } = await permissionService.getOrgPermission(
dto.actor,
dto.actorId,
org.id,
dto.organizationId,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
}
const oidcCfg = await oidcConfigDAL.findOne({
orgId: org.id
});
if (!oidcCfg) {
throw new NotFoundError({
message: `OIDC configuration for organization with slug '${dto.orgSlug}' not found`
});
}
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: oidcCfg.orgId
@ -465,7 +457,7 @@ export const oidcConfigServiceFactory = ({
};
const updateOidcCfg = async ({
orgSlug,
organizationId,
allowedEmailDomains,
configurationType,
discoveryURL,
@ -484,13 +476,11 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships,
jwtSignatureAlgorithm
}: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({
slug: orgSlug
});
const org = await orgDAL.findOne({ id: organizationId });
if (!org) {
throw new NotFoundError({
message: `Organization with slug '${orgSlug}' not found`
message: `Organization with ID '${organizationId}' not found`
});
}
@ -555,7 +545,7 @@ export const oidcConfigServiceFactory = ({
};
const createOidcCfg = async ({
orgSlug,
organizationId,
allowedEmailDomains,
configurationType,
discoveryURL,
@ -574,12 +564,10 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships,
jwtSignatureAlgorithm
}: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({
slug: orgSlug
});
const org = await orgDAL.findOne({ id: organizationId });
if (!org) {
throw new NotFoundError({
message: `Organization with slug '${orgSlug}' not found`
message: `Organization with ID '${organizationId}' not found`
});
}
@ -639,7 +627,7 @@ export const oidcConfigServiceFactory = ({
const oidcCfg = await getOidc({
type: "internal",
orgSlug
organizationId: org.id
});
if (!oidcCfg || !oidcCfg.isActive) {

View File

@ -26,11 +26,11 @@ export type TOidcLoginDTO = {
export type TGetOidcCfgDTO =
| ({
type: "external";
orgSlug: string;
organizationId: string;
} & TGenericPermission)
| {
type: "internal";
orgSlug: string;
organizationId: string;
};
export type TCreateOidcCfgDTO = {
@ -45,7 +45,7 @@ export type TCreateOidcCfgDTO = {
clientId: string;
clientSecret: string;
isActive: boolean;
orgSlug: string;
organizationId: string;
manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
} & TGenericPermission;
@ -62,7 +62,7 @@ export type TUpdateOidcCfgDTO = Partial<{
clientId: string;
clientSecret: string;
isActive: boolean;
orgSlug: string;
organizationId: string;
manageGroupMemberships: boolean;
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
}> &

View File

@ -148,10 +148,18 @@ export const samlConfigServiceFactory = ({
let samlConfig: TSamlConfigs | undefined;
if (dto.type === "org") {
samlConfig = await samlConfigDAL.findOne({ orgId: dto.orgId });
if (!samlConfig) return;
if (!samlConfig) {
throw new NotFoundError({
message: `SAML configuration for organization with ID '${dto.orgId}' not found`
});
}
} else if (dto.type === "orgSlug") {
const org = await orgDAL.findOne({ slug: dto.orgSlug });
if (!org) return;
if (!org) {
throw new NotFoundError({
message: `Organization with slug '${dto.orgSlug}' not found`
});
}
samlConfig = await samlConfigDAL.findOne({ orgId: org.id });
} else if (dto.type === "ssoId") {
// TODO:

View File

@ -61,20 +61,17 @@ export type TSamlLoginDTO = {
export type TSamlConfigServiceFactory = {
createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise<TSamlConfigs>;
updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise<TSamlConfigs>;
getSaml: (arg: TGetSamlCfgDTO) => Promise<
| {
id: string;
organization: string;
orgId: string;
authProvider: string;
isActive: boolean;
entryPoint: string;
issuer: string;
cert: string;
lastUsed: Date | null | undefined;
}
| undefined
>;
getSaml: (arg: TGetSamlCfgDTO) => Promise<{
id: string;
organization: string;
orgId: string;
authProvider: string;
isActive: boolean;
entryPoint: string;
issuer: string;
cert: string;
lastUsed: Date | null | undefined;
}>;
samlLogin: (arg: TSamlLoginDTO) => Promise<{
isUserCompleted: boolean;
providerAuthToken: string;

View File

@ -318,7 +318,7 @@ export const secretScanningV2QueueServiceFactory = async ({
},
{
batchSize: 1,
workerCount: 20,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
@ -539,7 +539,7 @@ export const secretScanningV2QueueServiceFactory = async ({
},
{
batchSize: 1,
workerCount: 20,
workerCount: 2,
pollingIntervalSeconds: 1
}
);
@ -613,7 +613,7 @@ export const secretScanningV2QueueServiceFactory = async ({
},
{
batchSize: 1,
workerCount: 5,
workerCount: 2,
pollingIntervalSeconds: 1
}
);

View File

@ -66,7 +66,10 @@ export enum ApiDocsTags {
KmsKeys = "KMS Keys",
KmsEncryption = "KMS Encryption",
KmsSigning = "KMS Signing",
SecretScanning = "Secret Scanning"
SecretScanning = "Secret Scanning",
OidcSso = "OIDC SSO",
SamlSso = "SAML SSO",
LdapSso = "LDAP SSO"
}
export const GROUPS = {
@ -2662,3 +2665,113 @@ export const SecretScanningConfigs = {
content: "The contents of the Secret Scanning Configuration file."
}
};
export const OidcSSo = {
GET_CONFIG: {
organizationId: "The ID of the organization to get the OIDC config for."
},
UPDATE_CONFIG: {
organizationId: "The ID of the organization to update the OIDC config for.",
allowedEmailDomains:
"A list of allowed email domains that users can use to authenticate with. This field is comma separated. Example: 'example.com,acme.com'",
discoveryURL: "The URL of the OIDC discovery endpoint.",
configurationType: "The configuration type to use for the OIDC configuration.",
issuer:
"The issuer for the OIDC configuration. This is only supported when the OIDC configuration type is set to 'custom'.",
authorizationEndpoint:
"The endpoint to use for OIDC authorization. This is only supported when the OIDC configuration type is set to 'custom'.",
jwksUri: "The URL of the OIDC JWKS endpoint.",
tokenEndpoint: "The token endpoint to use for OIDC token exchange.",
userinfoEndpoint: "The userinfo endpoint to get user information from the OIDC provider.",
clientId: "The client ID to use for OIDC authentication.",
clientSecret: "The client secret to use for OIDC authentication.",
isActive: "Whether to enable or disable this OIDC configuration.",
manageGroupMemberships:
"Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in, based on which groups they are a member of in the OIDC provider.",
jwtSignatureAlgorithm: "The algorithm to use for JWT signature verification."
},
CREATE_CONFIG: {
organizationId: "The ID of the organization to create the OIDC config for.",
allowedEmailDomains:
"A list of allowed email domains that users can use to authenticate with. This field is comma separated.",
discoveryURL: "The URL of the OIDC discovery endpoint.",
configurationType: "The configuration type to use for the OIDC configuration.",
issuer:
"The issuer for the OIDC configuration. This is only supported when the OIDC configuration type is set to 'custom'.",
authorizationEndpoint:
"The authorization endpoint to use for OIDC authorization. This is only supported when the OIDC configuration type is set to 'custom'.",
jwksUri: "The URL of the OIDC JWKS endpoint.",
tokenEndpoint: "The token endpoint to use for OIDC token exchange.",
userinfoEndpoint: "The userinfo endpoint to get user information from the OIDC provider.",
clientId: "The client ID to use for OIDC authentication.",
clientSecret: "The client secret to use for OIDC authentication.",
isActive: "Whether to enable or disable this OIDC configuration.",
manageGroupMemberships:
"Whether to manage group memberships for the OIDC configuration. If enabled, users will automatically be assigned groups when they sign in, based on which groups they are a member of in the OIDC provider.",
jwtSignatureAlgorithm: "The algorithm to use for JWT signature verification."
}
};
export const SamlSso = {
GET_CONFIG: {
organizationId: "The ID of the organization to get the SAML config for."
},
UPDATE_CONFIG: {
organizationId: "The ID of the organization to update the SAML config for.",
authProvider: "Authentication provider to use for SAML authentication.",
isActive: "Whether to enable or disable this SAML configuration.",
entryPoint:
"The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.",
issuer: "The SAML provider issuer URL or entity ID.",
cert: "The certificate to use for SAML authentication."
},
CREATE_CONFIG: {
organizationId: "The ID of the organization to create the SAML config for.",
authProvider: "Authentication provider to use for SAML authentication.",
isActive: "Whether to enable or disable this SAML configuration.",
entryPoint:
"The entry point for the SAML authentication. This is the URL that the user will be redirected to after they have authenticated with the SAML provider.",
issuer: "The SAML provider issuer URL or entity ID.",
cert: "The certificate to use for SAML authentication."
}
};
export const LdapSso = {
GET_CONFIG: {
organizationId: "The ID of the organization to get the LDAP config for."
},
CREATE_CONFIG: {
organizationId: "The ID of the organization to create the LDAP config for.",
isActive: "Whether to enable or disable this LDAP configuration.",
url: "The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.",
bindDN:
"The distinguished name of the object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`",
bindPass: "The password to use along with Bind DN when performing the user search.",
searchBase: "The base DN to use for the user search such as `ou=Users,dc=acme,dc=com`",
uniqueUserAttribute:
"The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID`. If left blank, defaults to uidNumber",
searchFilter:
"The template used to construct the LDAP user search filter such as `(uid={{username}})` uses literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.",
groupSearchBase: "LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`",
groupSearchFilter:
"The template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: `[UserDN, UserName]`. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.",
caCert: "The CA certificate to use when verifying the LDAP server certificate."
},
UPDATE_CONFIG: {
organizationId: "The ID of the organization to update the LDAP config for.",
isActive: "Whether to enable or disable this LDAP configuration.",
url: "The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` (for connection over SSL/TLS), etc.",
bindDN:
"The distinguished name of object to bind when performing the user search such as `cn=infisical,ou=Users,dc=acme,dc=com`",
bindPass: "The password to use along with Bind DN when performing the user search.",
uniqueUserAttribute:
"The attribute to use as the unique identifier of LDAP users such as `sAMAccountName`, `cn`, `uid`, `objectGUID`. If left blank, defaults to uidNumber",
searchFilter:
"The template used to construct the LDAP user search filter such as `(uid={{username}})` uses literal `{{username}}` to have the given username used in the search. The default is `(uid={{username}})` which is compatible with several common directory schemas.",
searchBase: "The base DN to use for the user search such as `ou=Users,dc=acme,dc=com`",
groupSearchBase: "LDAP search base to use for group membership search such as `ou=Groups,dc=acme,dc=com`",
groupSearchFilter:
"The template used when constructing the group membership query such as `(&(objectClass=posixGroup)(memberUid={{.Username}}))`. The template can access the following context variables: `[UserDN, UserName]`. The default is `(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))` which is compatible with several common directory schemas.",
caCert: "The CA certificate to use when verifying the LDAP server certificate."
}
};

View File

@ -2,6 +2,7 @@ import { z } from "zod";
import { QueueWorkerProfile } from "@app/lib/types";
import { BadRequestError } from "../errors";
import { removeTrailingSlash } from "../fn";
import { CustomLogger } from "../logger/logger";
import { zpStr } from "../zod";
@ -341,8 +342,11 @@ const envSchema = z
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
let envCfg: TEnvConfig;
let originalEnvConfig: TEnvConfig;
export const getConfig = () => envCfg;
export const getOriginalConfig = () => originalEnvConfig;
// cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger?: CustomLogger) => {
const parsedEnv = envSchema.safeParse(process.env);
@ -352,10 +356,115 @@ export const initEnvConfig = (logger?: CustomLogger) => {
process.exit(-1);
}
envCfg = Object.freeze(parsedEnv.data);
const config = Object.freeze(parsedEnv.data);
envCfg = config;
if (!originalEnvConfig) {
originalEnvConfig = config;
}
return envCfg;
};
// A list of environment variables that can be overwritten
export const overwriteSchema: {
[key: string]: {
name: string;
fields: { key: keyof TEnvConfig; description?: string }[];
};
} = {
azure: {
name: "Azure",
fields: [
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_ID",
description: "The Application (Client) ID of your Azure application."
},
{
key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET",
description: "The Client Secret of your Azure application."
}
]
},
google_sso: {
name: "Google SSO",
fields: [
{
key: "CLIENT_ID_GOOGLE_LOGIN",
description: "The Client ID of your GCP OAuth2 application."
},
{
key: "CLIENT_SECRET_GOOGLE_LOGIN",
description: "The Client Secret of your GCP OAuth2 application."
}
]
},
github_sso: {
name: "GitHub SSO",
fields: [
{
key: "CLIENT_ID_GITHUB_LOGIN",
description: "The Client ID of your GitHub OAuth application."
},
{
key: "CLIENT_SECRET_GITHUB_LOGIN",
description: "The Client Secret of your GitHub OAuth application."
}
]
},
gitlab_sso: {
name: "GitLab SSO",
fields: [
{
key: "CLIENT_ID_GITLAB_LOGIN",
description: "The Client ID of your GitLab application."
},
{
key: "CLIENT_SECRET_GITLAB_LOGIN",
description: "The Secret of your GitLab application."
},
{
key: "CLIENT_GITLAB_LOGIN_URL",
description:
"The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to https://gitlab.com."
}
]
}
};
export const overridableKeys = new Set(
Object.values(overwriteSchema).flatMap(({ fields }) => fields.map(({ key }) => key))
);
export const validateOverrides = (config: Record<string, string>) => {
const allowedOverrides = Object.fromEntries(
Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input<typeof envSchema>))
);
const tempEnv: Record<string, unknown> = { ...process.env, ...allowedOverrides };
const parsedResult = envSchema.safeParse(tempEnv);
if (!parsedResult.success) {
const errorDetails = parsedResult.error.issues
.map((issue) => `Key: "${issue.path.join(".")}", Error: ${issue.message}`)
.join("\n");
throw new BadRequestError({ message: errorDetails });
}
};
export const overrideEnvConfig = (config: Record<string, string>) => {
const allowedOverrides = Object.fromEntries(
Object.entries(config).filter(([key]) => overridableKeys.has(key as keyof z.input<typeof envSchema>))
);
const tempEnv: Record<string, unknown> = { ...process.env, ...allowedOverrides };
const parsedResult = envSchema.safeParse(tempEnv);
if (parsedResult.success) {
envCfg = Object.freeze(parsedResult.data);
}
};
export const formatSmtpConfig = () => {
const tlsOptions: {
rejectUnauthorized: boolean;

View File

@ -300,6 +300,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerV1Routes } from "./v1";
import { initializeOauthConfigSync } from "./v1/sso-router";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
@ -1615,7 +1616,8 @@ export const registerRoutes = async (
secretSharingDAL,
secretVersionV2DAL: secretVersionV2BridgeDAL,
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL,
serviceTokenService
serviceTokenService,
orgService
});
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
@ -1909,6 +1911,7 @@ export const registerRoutes = async (
await hsmService.startService();
await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
await pkiSubscriberQueue.startDailyAutoRenewalJob();
@ -2045,6 +2048,16 @@ export const registerRoutes = async (
}
}
const configSyncJob = await superAdminService.initializeEnvConfigSync();
if (configSyncJob) {
cronJobs.push(configSyncJob);
}
const oauthConfigSyncJob = await initializeOauthConfigSync();
if (oauthConfigSyncJob) {
cronJobs.push(oauthConfigSyncJob);
}
server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL,
kmipClient: kmipClientDAL

View File

@ -8,7 +8,7 @@ import {
SuperAdminSchema,
UsersSchema
} from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { getConfig, overridableKeys } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@ -42,7 +42,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
encryptedGitHubAppConnectionClientSecret: true,
encryptedGitHubAppConnectionSlug: true,
encryptedGitHubAppConnectionId: true,
encryptedGitHubAppConnectionPrivateKey: true
encryptedGitHubAppConnectionPrivateKey: true,
encryptedEnvOverrides: true
}).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
@ -110,11 +111,14 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
.refine((content) => DOMPurify.sanitize(content) === content, {
message: "Page frame content contains unsafe HTML."
})
.optional()
.optional(),
envOverrides: z.record(z.enum(Array.from(overridableKeys) as [string, ...string[]]), z.string()).optional()
}),
response: {
200: z.object({
config: SuperAdminSchema.extend({
config: SuperAdminSchema.omit({
encryptedEnvOverrides: true
}).extend({
defaultAuthOrgSlug: z.string().nullable()
})
})
@ -381,6 +385,41 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/env-overrides",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.record(
z.string(),
z.object({
name: z.string(),
fields: z
.object({
key: z.string(),
value: z.string(),
hasEnvEntry: z.boolean(),
description: z.string().optional()
})
.array()
})
)
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const envOverrides = await server.services.superAdmin.getEnvOverridesOrganized();
return envOverrides;
}
});
server.route({
method: "DELETE",
url: "/user-management/users/:userId",

View File

@ -113,52 +113,73 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
hide: false,
tags: [ApiDocsTags.AuditLogs],
description: "Get all audit logs for an organization",
querystring: z.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
actorType: z.nativeEnum(ActorType).optional(),
secretPath: z
.string()
.optional()
.transform((val) => (!val ? val : removeTrailingSlash(val)))
.describe(AUDIT_LOGS.EXPORT.secretPath),
secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey),
querystring: z
.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
environment: z.string().optional().describe(AUDIT_LOGS.EXPORT.environment),
actorType: z.nativeEnum(ActorType).optional(),
secretPath: z
.string()
.optional()
.transform((val) => (!val ? val : removeTrailingSlash(val)))
.describe(AUDIT_LOGS.EXPORT.secretPath),
secretKey: z.string().optional().describe(AUDIT_LOGS.EXPORT.secretKey),
// eventType is split with , for multiple values, we need to transform it to array
eventType: z
.string()
.optional()
.transform((val) => (val ? val.split(",") : undefined)),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
eventMetadata: z
.string()
.optional()
.transform((val) => {
if (!val) {
return undefined;
}
// eventType is split with , for multiple values, we need to transform it to array
eventType: z
.string()
.optional()
.transform((val) => (val ? val.split(",") : undefined)),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
eventMetadata: z
.string()
.optional()
.transform((val) => {
if (!val) {
return undefined;
const pairs = val.split(",");
return pairs.reduce(
(acc, pair) => {
const [key, value] = pair.split("=");
if (key && value) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
})
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
limit: z.coerce.number().max(1000).default(20).describe(AUDIT_LOGS.EXPORT.limit),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
})
.superRefine((el, ctx) => {
if (el.endDate && el.startDate) {
const startDate = new Date(el.startDate);
const endDate = new Date(el.endDate);
const maxAllowedDate = new Date(startDate);
maxAllowedDate.setMonth(maxAllowedDate.getMonth() + 3);
if (endDate < startDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "End date cannot be before start date"
});
}
const pairs = val.split(",");
return pairs.reduce(
(acc, pair) => {
const [key, value] = pair.split("=");
if (key && value) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
})
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
limit: z.coerce.number().default(20).describe(AUDIT_LOGS.EXPORT.limit),
actor: z.string().optional().describe(AUDIT_LOGS.EXPORT.actor)
}),
if (endDate > maxAllowedDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["endDate"],
message: "Dates must be within 3 months"
});
}
}
}),
response: {
200: z.object({
auditLogs: AuditLogsSchema.omit({
@ -188,14 +209,13 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
const auditLogs = await server.services.auditLog.listAuditLogs({
filter: {
...req.query,
endDate: req.query.endDate,
endDate: req.query.endDate || new Date().toISOString(),
projectId: req.query.projectId,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
actorType: req.query.actorType,
eventType: req.query.eventType as EventType[] | undefined
},
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,

View File

@ -382,7 +382,8 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
{
syncId,
destination,
importBehavior
importBehavior,
auditLogInfo: req.auditLogInfo
},
req.permission
)) as T;
@ -415,7 +416,8 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
const secretSync = (await server.services.secretSync.triggerSecretSyncRemoveSecretsById(
{
syncId,
destination
destination,
auditLogInfo: req.auditLogInfo
},
req.permission
)) as T;

View File

@ -9,6 +9,7 @@
import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session";
import RedisStore from "connect-redis";
import { CronJob } from "cron";
import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as OAuth2Strategy } from "passport-oauth2";
@ -25,27 +26,14 @@ import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
export const registerSsoRouter = async (server: FastifyZodProvider) => {
const passport = new Authenticator({ key: "sso", userProperty: "passportUser" });
let serverInstance: FastifyZodProvider | null = null;
export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
serverInstance = server;
const appCfg = getConfig();
const passport = new Authenticator({ key: "sso", userProperty: "passportUser" });
const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
// passport oauth strategy for Google
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
if (isGoogleOauthActive) {
@ -176,6 +164,49 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
)
);
}
};
export const refreshOauthConfig = () => {
if (!serverInstance) {
logger.warn("Cannot refresh OAuth config: server instance not available");
return;
}
logger.info("Refreshing OAuth configuration...");
registerOauthMiddlewares(serverInstance);
};
export const initializeOauthConfigSync = async () => {
logger.info("Setting up background sync process for oauth configuration");
// sync every 5 minutes
const job = new CronJob("*/5 * * * *", refreshOauthConfig);
job.start();
return job;
};
export const registerSsoRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
registerOauthMiddlewares(server);
server.route({
url: "/redirect/google",

View File

@ -93,23 +93,25 @@ export const identityProjectServiceFactory = ({
projectId
);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to assign to role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (requestedRoleChange !== ProjectMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to assign to role",
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
ProjectPermissionSub.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
}
// validate custom roles input

View File

@ -69,23 +69,25 @@ export const identityServiceFactory = ({
orgId
);
const isCustomRole = Boolean(customRole);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to create identity",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
if (role !== OrgMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to create identity",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
const plan = await licenseService.getPlan(orgId);
@ -187,6 +189,7 @@ export const identityServiceFactory = ({
),
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
});
if (isCustomRole) customRole = customOrgRole;
}

View File

@ -103,8 +103,54 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
}
};
const findRecentInvitedMemberships = async () => {
try {
const now = new Date();
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const threeMonthsAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
const memberships = await db
.replicaNode()(TableName.OrgMembership)
.where("status", "invited")
.where((qb) => {
// lastInvitedAt is null AND createdAt is between 1 week and 3 months ago
void qb
.whereNull(`${TableName.OrgMembership}.lastInvitedAt`)
.whereBetween(`${TableName.OrgMembership}.createdAt`, [threeMonthsAgo, oneWeekAgo]);
})
.orWhere((qb) => {
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
void qb
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo)
.where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo);
});
return memberships;
} catch (error) {
throw new DatabaseError({
error,
name: "Find recent invited memberships"
});
}
};
const updateLastInvitedAtByIds = async (membershipIds: string[]) => {
try {
if (membershipIds.length === 0) return;
await db(TableName.OrgMembership).whereIn("id", membershipIds).update({ lastInvitedAt: new Date() });
} catch (error) {
throw new DatabaseError({
error,
name: "Update last invited at by ids"
});
}
};
return {
...orgMembershipOrm,
findOrgMembershipById
findOrgMembershipById,
findRecentInvitedMemberships,
updateLastInvitedAtByIds
};
};

View File

@ -107,7 +107,15 @@ type TOrgServiceFactoryDep = {
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
orgMembershipDAL: Pick<
TOrgMembershipDALFactory,
| "findOrgMembershipById"
| "findOne"
| "findById"
| "findRecentInvitedMemberships"
| "updateById"
| "updateLastInvitedAtByIds"
>;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
@ -760,6 +768,10 @@ export const orgServiceFactory = ({
}
});
await orgMembershipDAL.updateById(inviteeOrgMembership.id, {
lastInvitedAt: new Date()
});
return { signupToken: undefined };
};
@ -1422,6 +1434,53 @@ export const orgServiceFactory = ({
return incidentContact;
};
/**
* Re-send emails to users who haven't accepted an invite yet
*/
const notifyInvitedUsers = async () => {
const invitedUsers = await orgMembershipDAL.findRecentInvitedMemberships();
const appCfg = getConfig();
const orgCache: Record<string, { name: string; id: string } | undefined> = {};
const notifiedUsers: string[] = [];
await Promise.all(
invitedUsers.map(async (invitedUser) => {
let org = orgCache[invitedUser.orgId];
if (!org) {
org = await orgDAL.findById(invitedUser.orgId);
orgCache[invitedUser.orgId] = org;
}
if (!org || !invitedUser.userId) return;
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: invitedUser.userId,
orgId: org.id
});
if (invitedUser.inviteEmail) {
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: `Reminder: You have been invited to ${org.name} on Infisical`,
recipients: [invitedUser.inviteEmail],
substitutions: {
organizationName: org.name,
email: invitedUser.inviteEmail,
organizationId: org.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
notifiedUsers.push(invitedUser.id);
}
})
);
await orgMembershipDAL.updateLastInvitedAtByIds(notifiedUsers);
};
return {
findOrganizationById,
findAllOrgMembers,
@ -1445,6 +1504,7 @@ export const orgServiceFactory = ({
listProjectMembershipsByOrgMembershipId,
findOrgBySlug,
resendOrgMemberInvitation,
upgradePrivilegeSystem
upgradePrivilegeSystem,
notifyInvitedUsers
};
};

View File

@ -5,6 +5,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
@ -24,6 +25,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
queueService: TQueueServiceFactory;
orgService: TOrgServiceFactory;
};
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
@ -39,12 +41,12 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
secretSharingDAL,
secretVersionV2DAL,
identityUniversalAuthClientSecretDAL,
serviceTokenService
serviceTokenService,
orgService
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
await secretDAL.pruneSecretReminders(queueService);
await auditLogDAL.pruneAuditLog();
await identityAccessTokenDAL.removeExpiredTokens();
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
await secretSharingDAL.pruneExpiredSharedSecrets();
@ -54,6 +56,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiringTokens();
await orgService.notifyInvitedUsers();
await auditLogDAL.pruneAuditLog();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
});

View File

@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import path from "path";
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
import { TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas";
import { TProjectEnvironments, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -469,15 +469,41 @@ export const secretFolderServiceFactory = ({
const $checkFolderPolicy = async ({
projectId,
environment,
parentId
env,
parentId,
idOrName
}: {
projectId: string;
environment: string;
env: TProjectEnvironments;
parentId: string;
idOrName: string;
}) => {
let targetFolder = await folderDAL
.findOne({
envId: env.id,
name: idOrName,
parentId,
isReserved: false
})
.catch(() => null);
if (!targetFolder && uuidValidate(idOrName)) {
targetFolder = await folderDAL
.findOne({
envId: env.id,
id: idOrName,
parentId,
isReserved: false
})
.catch(() => null);
}
if (!targetFolder) {
throw new NotFoundError({ message: `Target folder not found` });
}
// get environment root folder (as it's needed to get all folders under it)
const rootFolder = await folderDAL.findBySecretPath(projectId, environment, "/");
const rootFolder = await folderDAL.findBySecretPath(projectId, env.slug, "/");
if (!rootFolder) throw new NotFoundError({ message: `Root folder not found` });
// get all folders under environment root folder
const folderPaths = await folderDAL.findByEnvsDeep({ parentIds: [rootFolder.id] });
@ -492,7 +518,13 @@ export const secretFolderServiceFactory = ({
folderMap.get(normalizeKey(folder.parentId))?.push(folder);
}
// Recursively collect all folders under the given parentId
// Find the target folder in the folderPaths to get its full details
const targetFolderWithPath = folderPaths.find((f) => f.id === targetFolder!.id);
if (!targetFolderWithPath) {
throw new NotFoundError({ message: `Target folder path not found` });
}
// Recursively collect all folders under the target folder (descendants only)
const collectDescendants = (
id: string
): (TSecretFolders & { path: string; depth: number; environment: string })[] => {
@ -500,23 +532,31 @@ export const secretFolderServiceFactory = ({
return [...children, ...children.flatMap((child) => collectDescendants(child.id))];
};
const foldersUnderParent = collectDescendants(parentId);
const targetFolderDescendants = collectDescendants(targetFolder.id);
const folderPolicyPaths = foldersUnderParent.map((folder) => ({
// Include the target folder itself plus all its descendants
const foldersToCheck = [targetFolderWithPath, ...targetFolderDescendants];
const folderPolicyPaths = foldersToCheck.map((folder) => ({
path: folder.path,
id: folder.id
}));
// get secrets under the given folders
const secrets = await secretV2BridgeDAL.findByFolderIds({ folderIds: folderPolicyPaths.map((p) => p.id) });
const secrets = await secretV2BridgeDAL.findByFolderIds({
folderIds: folderPolicyPaths.map((p) => p.id)
});
for await (const folderPolicyPath of folderPolicyPaths) {
// eslint-disable-next-line no-continue
if (!secrets.some((s) => s.folderId === folderPolicyPath.id)) continue;
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
projectId,
environment,
env.slug,
folderPolicyPath.path
);
// if there is a policy and there are secrets under the given folder, throw error
if (policy) {
throw new BadRequestError({
@ -560,20 +600,42 @@ export const secretFolderServiceFactory = ({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
});
await $checkFolderPolicy({ projectId, environment, parentId: parentFolder.id });
await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName });
let folderToDelete = await folderDAL
.findOne({
envId: env.id,
name: idOrName,
parentId: parentFolder.id,
isReserved: false
})
.catch(() => null);
if (!folderToDelete && uuidValidate(idOrName)) {
folderToDelete = await folderDAL
.findOne({
envId: env.id,
id: idOrName,
parentId: parentFolder.id,
isReserved: false
})
.catch(() => null);
}
if (!folderToDelete) {
throw new NotFoundError({ message: `Folder with ID '${idOrName}' not found` });
}
const [doc] = await folderDAL.delete(
{
envId: env.id,
[uuidValidate(idOrName) ? "id" : "name"]: idOrName,
id: folderToDelete.id,
parentId: parentFolder.id,
isReserved: false
},
tx
);
if (!doc) throw new NotFoundError({ message: `Failed to delete folder with ID '${idOrName}', not found` });
const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id], tx);
await folderCommitService.createCommit(

View File

@ -5,7 +5,13 @@ import jwt from "jsonwebtoken";
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import {
getConfig,
getOriginalConfig,
overrideEnvConfig,
overwriteSchema,
validateOverrides
} from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -33,6 +39,7 @@ import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import {
CacheType,
EnvOverrides,
LoginMethod,
TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO,
@ -234,6 +241,45 @@ export const superAdminServiceFactory = ({
adminIntegrationsConfig = config;
};
const getEnvOverrides = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg || !serverCfg.encryptedEnvOverrides) {
return {};
}
const decrypt = kmsService.decryptWithRootKey();
const overrides = JSON.parse(decrypt(serverCfg.encryptedEnvOverrides).toString()) as Record<string, string>;
return overrides;
};
const getEnvOverridesOrganized = async (): Promise<EnvOverrides> => {
const overrides = await getEnvOverrides();
const ogConfig = getOriginalConfig();
return Object.fromEntries(
Object.entries(overwriteSchema).map(([groupKey, groupDef]) => [
groupKey,
{
name: groupDef.name,
fields: groupDef.fields.map(({ key, description }) => ({
key,
description,
value: overrides[key] || "",
hasEnvEntry: !!(ogConfig as unknown as Record<string, string | undefined>)[key]
}))
}
])
);
};
const $syncEnvConfig = async () => {
const config = await getEnvOverrides();
overrideEnvConfig(config);
};
const updateServerCfg = async (
data: TSuperAdminUpdate & {
slackClientId?: string;
@ -246,6 +292,7 @@ export const superAdminServiceFactory = ({
gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string;
envOverrides?: Record<string, string>;
},
userId: string
) => {
@ -374,6 +421,17 @@ export const superAdminServiceFactory = ({
gitHubAppConnectionSettingsUpdated = true;
}
let envOverridesUpdated = false;
if (data.envOverrides !== undefined) {
// Verify input format
validateOverrides(data.envOverrides);
const encryptedEnvOverrides = encryptWithRoot(Buffer.from(JSON.stringify(data.envOverrides)));
updatedData.encryptedEnvOverrides = encryptedEnvOverrides;
updatedData.envOverrides = undefined;
envOverridesUpdated = true;
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
@ -382,6 +440,10 @@ export const superAdminServiceFactory = ({
await $syncAdminIntegrationConfig();
}
if (envOverridesUpdated) {
await $syncEnvConfig();
}
if (
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
@ -814,6 +876,18 @@ export const superAdminServiceFactory = ({
return job;
};
const initializeEnvConfigSync = async () => {
logger.info("Setting up background sync process for environment overrides");
await $syncEnvConfig();
// sync every 5 minutes
const job = new CronJob("*/5 * * * *", $syncEnvConfig);
job.start();
return job;
};
return {
initServerCfg,
updateServerCfg,
@ -833,6 +907,9 @@ export const superAdminServiceFactory = ({
getOrganizations,
deleteOrganization,
deleteOrganizationMembership,
initializeAdminIntegrationConfigSync
initializeAdminIntegrationConfigSync,
initializeEnvConfigSync,
getEnvOverrides,
getEnvOverridesOrganized
};
};

View File

@ -1,3 +1,5 @@
import { TEnvConfig } from "@app/lib/config/env";
export type TAdminSignUpDTO = {
email: string;
password: string;
@ -74,3 +76,10 @@ export type TAdminIntegrationConfig = {
privateKey: string;
};
};
export interface EnvOverrides {
[key: string]: {
name: string;
fields: { key: keyof TEnvConfig; value: string; hasEnvEntry: boolean; description?: string }[];
};
}

View File

@ -71,6 +71,15 @@ export const telemetryQueueServiceFactory = ({
QueueName.TelemetryInstanceStats // just a job id
);
if (postHog) {
await queueService.queue(QueueName.TelemetryInstanceStats, QueueJobs.TelemetryInstanceStats, undefined, {
jobId: QueueName.TelemetryInstanceStats,
repeat: { pattern: "0 0 * * *", utc: true }
});
}
};
const startAggregatedEventsJob = async () => {
// clear previous aggregated events job
await queueService.stopRepeatableJob(
QueueName.TelemetryAggregatedEvents,
@ -80,11 +89,6 @@ export const telemetryQueueServiceFactory = ({
);
if (postHog) {
await queueService.queue(QueueName.TelemetryInstanceStats, QueueJobs.TelemetryInstanceStats, undefined, {
jobId: QueueName.TelemetryInstanceStats,
repeat: { pattern: "0 0 * * *", utc: true }
});
// Start aggregated events job (runs every five minutes)
await queueService.queue(QueueName.TelemetryAggregatedEvents, QueueJobs.TelemetryAggregatedEvents, undefined, {
jobId: QueueName.TelemetryAggregatedEvents,
@ -102,6 +106,7 @@ export const telemetryQueueServiceFactory = ({
});
return {
startTelemetryCheck
startTelemetryCheck,
startAggregatedEventsJob
};
};

View File

@ -14,7 +14,7 @@ export const TELEMETRY_SECRET_PROCESSED_KEY = "telemetry-secret-processed";
export const TELEMETRY_SECRET_OPERATIONS_KEY = "telemetry-secret-operations";
export const POSTHOG_AGGREGATED_EVENTS = [PostHogEventTypes.SecretPulled];
const TELEMETRY_AGGREGATED_KEY_EXP = 900; // 15mins
const TELEMETRY_AGGREGATED_KEY_EXP = 600; // 10mins
// Bucket configuration
const TELEMETRY_BUCKET_COUNT = 30;
@ -102,13 +102,6 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
const instanceType = licenseService.getInstanceType();
// capture posthog only when its cloud or signup event happens in self-hosted
if (instanceType === InstanceType.Cloud || event.event === PostHogEventTypes.UserSignedUp) {
if (event.organizationId) {
try {
postHog.groupIdentify({ groupType: "organization", groupKey: event.organizationId });
} catch (error) {
logger.error(error, "Failed to identify PostHog organization");
}
}
if (POSTHOG_AGGREGATED_EVENTS.includes(event.event)) {
const eventKey = createTelemetryEventKey(event.event, event.distinctId);
await keyStore.setItemWithExpiry(
@ -122,6 +115,13 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
})
);
} else {
if (event.organizationId) {
try {
postHog.groupIdentify({ groupType: "organization", groupKey: event.organizationId });
} catch (error) {
logger.error(error, "Failed to identify PostHog organization");
}
}
postHog.capture({
event: event.event,
distinctId: event.distinctId,

View File

@ -35,6 +35,7 @@ const (
GitHubPlatform
GitLabPlatform
AzureDevOpsPlatform
BitBucketPlatform
// TODO: Add others.
)
@ -45,6 +46,7 @@ func (p Platform) String() string {
"github",
"gitlab",
"azuredevops",
"bitbucket",
}[p]
}
@ -60,6 +62,8 @@ func PlatformFromString(s string) (Platform, error) {
return GitLabPlatform, nil
case "azuredevops":
return AzureDevOpsPlatform, nil
case "bitbucket":
return BitBucketPlatform, nil
default:
return UnknownPlatform, fmt.Errorf("invalid scm platform value: %s", s)
}

View File

@ -208,6 +208,8 @@ func platformFromHost(u *url.URL) scm.Platform {
return scm.GitLabPlatform
case "dev.azure.com", "visualstudio.com":
return scm.AzureDevOpsPlatform
case "bitbucket.org":
return scm.BitBucketPlatform
default:
return scm.UnknownPlatform
}

View File

@ -112,6 +112,15 @@ func createScmLink(scmPlatform scm.Platform, remoteUrl string, finding report.Fi
// This is a bit dirty, but Azure DevOps does not highlight the line when the lineStartColumn and lineEndColumn are not provided
link += "&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files"
return link
case scm.BitBucketPlatform:
link := fmt.Sprintf("%s/src/%s/%s", remoteUrl, finding.Commit, filePath)
if finding.StartLine != 0 {
link += fmt.Sprintf("#lines-%d", finding.StartLine)
}
if finding.EndLine != finding.StartLine {
link += fmt.Sprintf(":%d", finding.EndLine)
}
return link
default:
// This should never happen.
return ""

View File

@ -337,9 +337,7 @@ var scanCmd = &cobra.Command{
if gitCmd, err = sources.NewGitLogCmd(source, logOpts); err != nil {
logging.Fatal().Err(err).Msg("could not create Git cmd")
}
if scmPlatform, err = scm.PlatformFromString("github"); err != nil {
logging.Fatal().Err(err).Send()
}
scmPlatform = scm.UnknownPlatform
remote = detect.NewRemoteInfo(scmPlatform, source)
if findings, err = detector.DetectGit(gitCmd, remote); err != nil {

View File

@ -0,0 +1,4 @@
---
title: "Create LDAP SSO Config"
openapi: "POST /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get LDAP SSO Config"
openapi: "GET /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update LDAP SSO Config"
openapi: "PATCH /api/v1/ldap/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Create OIDC Config"
openapi: "POST /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get OIDC Config"
openapi: "GET /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update OIDC Config"
openapi: "PATCH /api/v1/sso/oidc/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Create SAML SSO Config"
openapi: "POST /api/v1/sso/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Get SAML SSO Config"
openapi: "GET /api/v1/sso/config"
---

View File

@ -0,0 +1,4 @@
---
title: "Update SAML SSO Config"
openapi: "PATCH /api/v1/sso/config"
---

View File

@ -851,6 +851,30 @@
{
"group": "Organizations",
"pages": [
{
"group": "OIDC SSO",
"pages": [
"api-reference/endpoints/organizations/oidc-sso/get-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/update-oidc-config",
"api-reference/endpoints/organizations/oidc-sso/create-oidc-config"
]
},
{
"group": "LDAP SSO",
"pages": [
"api-reference/endpoints/organizations/ldap-sso/get-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/update-ldap-config",
"api-reference/endpoints/organizations/ldap-sso/create-ldap-config"
]
},
{
"group": "SAML SSO",
"pages": [
"api-reference/endpoints/organizations/saml-sso/get-saml-config",
"api-reference/endpoints/organizations/saml-sso/update-saml-config",
"api-reference/endpoints/organizations/saml-sso/create-saml-config"
]
},
"api-reference/endpoints/organizations/memberships",
"api-reference/endpoints/organizations/update-membership",
"api-reference/endpoints/organizations/delete-membership",

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

View File

@ -794,3 +794,9 @@ If export type is set to `otlp`, you will have to configure a value for `OTEL_EX
The TLS header used to propagate the client certificate from the load balancer
to the server.
</ParamField>
## Environment Variable Overrides
If you can't directly access and modify environment variables, you can update them using the [Server Admin Console](/documentation/platform/admin-panel/server-admin).
![Environment Variables Overrides Page](../../images/self-hosting/configuration/overrides/page.png)

View File

@ -65,6 +65,7 @@ export const createNotification = (
toast(<NotificationContent {...myProps} />, {
position: "bottom-right",
...toastProps,
autoClose: toastProps.autoClose || 15000,
theme: "dark",
type: myProps?.type || "info"
});

View File

@ -23,6 +23,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
const { control, watch } = useFormContext<TSecretSyncForm>();
const destination = watch("destination");
const currentSyncOption = watch("syncOptions");
const destinationName = SECRET_SYNC_MAP[destination].name;
@ -128,8 +129,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
{!syncOption?.canImportSecrets && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
{destinationName} only supports overwriting destination secrets. Secrets not present
in Infisical will be removed from the destination.
{destinationName} only supports overwriting destination secrets.{" "}
{!currentSyncOption.disableSecretDeletion &&
"Secrets not present in Infisical will be removed from the destination."}
</p>
)}
</>

View File

@ -33,7 +33,7 @@ export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
className
)}
>
<Lottie icon="infisical_loading" className="h-32 w-32" />
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
{text && isTextArray && (
<AnimatePresence mode="wait">
<motion.div

View File

@ -0,0 +1,36 @@
import { MouseEventHandler } from "react";
export const HeaderResizer = ({
onMouseDown,
isActive,
scrollOffset,
heightOffset
}: {
onMouseDown: MouseEventHandler<HTMLDivElement>;
isActive: boolean;
scrollOffset: number;
heightOffset: number;
}) => {
return (
<>
<div
tabIndex={-1}
role="button"
className={`absolute left-0 z-40 h-0.5 w-full cursor-ns-resize hover:bg-blue-400/20 ${
isActive ? "bg-blue-400/75" : "bg-transparent"
}`}
onMouseDown={onMouseDown}
style={{
transform: "translateY(50%)",
top: heightOffset
}}
/>
<div
style={{ left: `calc(50% + ${scrollOffset}px)`, top: heightOffset }}
className="pointer-events-none absolute z-30 -translate-x-1/2"
>
<div className="h-1 w-8 rounded bg-gray-400 opacity-50" />
</div>
</>
);
};

View File

@ -0,0 +1,42 @@
export const HighlightText = ({
text,
highlight,
highlightClassName
}: {
text: string | undefined | null;
highlight: string;
highlightClassName?: string;
}) => {
if (!text) return null;
const searchTerm = highlight.toLowerCase().trim();
if (!searchTerm) return <span>{text}</span>;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(escapedSearchTerm, "gi");
text.replace(regex, (match: string, offset: number) => {
if (offset > lastIndex) {
parts.push(<span key={`pre-${lastIndex}`}>{text.substring(lastIndex, offset)}</span>);
}
parts.push(
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
{match}
</span>
);
lastIndex = offset + match.length;
return match;
});
if (lastIndex < text.length) {
parts.push(<span key={`post-${lastIndex}`}>{text.substring(lastIndex)}</span>);
}
return parts;
};

View File

@ -0,0 +1 @@
export { HighlightText } from "./HighlightText";

View File

@ -45,10 +45,14 @@ export const Table = ({ children, className }: TableProps): JSX.Element => (
export type THeadProps = {
children: ReactNode;
className?: string;
style?: React.CSSProperties;
};
export const THead = ({ children, className }: THeadProps): JSX.Element => (
<thead className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}>
export const THead = ({ children, className, style }: THeadProps): JSX.Element => (
<thead
className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}
style={style}
>
{children}
</thead>
);
@ -96,14 +100,16 @@ export const Tr = ({
export type ThProps = {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
export const Th = ({ children, className, style }: ThProps): JSX.Element => (
<th
className={twMerge(
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pb-3.5 pt-4 font-semibold",
className
)}
style={style}
>
{children}
</th>

View File

@ -43,23 +43,25 @@ export const Tooltip = ({
onOpenChange={onOpenChange}
>
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Content
side={position}
align="center"
sideOffset={5}
{...props}
className={twMerge(
"z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade",
isDisabled && "!hidden",
center && "text-center",
size === "sm" && "rounded-sm px-2 py-1 text-xs",
size === "md" && "rounded-md px-4 py-2 text-sm",
className
)}
>
{content}
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
</TooltipPrimitive.Content>
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
side={position}
align="center"
sideOffset={5}
{...props}
className={twMerge(
"z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade",
isDisabled && "!hidden",
center && "text-center",
size === "sm" && "rounded-sm px-2 py-1 text-xs",
size === "md" && "rounded-md px-4 py-2 text-sm",
className
)}
>
{content}
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
</TooltipPrimitive.Root>
) : (
// eslint-disable-next-line react/jsx-no-useless-fragment

View File

@ -10,6 +10,7 @@ import {
AdminGetUsersFilters,
AdminIntegrationsConfig,
OrganizationWithProjects,
TGetEnvOverrides,
TGetInvalidatingCacheStatus,
TGetServerRootKmsEncryptionDetails,
TServerConfig
@ -31,7 +32,8 @@ export const adminQueryKeys = {
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getEnvOverrides: () => ["env-overrides"] as const
};
export const fetchServerConfig = async () => {
@ -163,3 +165,13 @@ export const useGetInvalidatingCacheStatus = (enabled = true) => {
refetchInterval: (data) => (data ? 3000 : false)
});
};
export const useGetEnvOverrides = () => {
return useQuery({
queryKey: adminQueryKeys.getEnvOverrides(),
queryFn: async () => {
const { data } = await apiRequest.get<TGetEnvOverrides>("/api/v1/admin/env-overrides");
return data;
}
});
};

View File

@ -48,6 +48,7 @@ export type TServerConfig = {
authConsentContent?: string;
pageFrameContent?: string;
invalidatingCache: boolean;
envOverrides?: Record<string, string>;
};
export type TUpdateServerConfigDTO = {
@ -61,6 +62,7 @@ export type TUpdateServerConfigDTO = {
gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string;
envOverrides?: Record<string, string>;
} & Partial<TServerConfig>;
export type TCreateAdminUserDTO = {
@ -138,3 +140,10 @@ export type TInvalidateCacheDTO = {
export type TGetInvalidatingCacheStatus = {
invalidating: boolean;
};
export interface TGetEnvOverrides {
[key: string]: {
name: string;
fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[];
};
}

View File

@ -30,8 +30,8 @@ export const useGetAuditLogs = (
params: {
...filters,
offset: pageParam,
startDate: filters?.startDate?.toISOString(),
endDate: filters?.endDate?.toISOString(),
startDate: filters.startDate.toISOString(),
endDate: filters.endDate.toISOString(),
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)

View File

@ -14,8 +14,8 @@ export type TGetAuditLogsFilter = {
actor?: string; // user ID format
secretPath?: string;
secretKey?: string;
startDate?: Date;
endDate?: Date;
startDate: Date;
endDate: Date;
limit: number;
};

View File

@ -21,7 +21,7 @@ export const useUpdateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug,
organizationId,
manageGroupMemberships,
jwtSignatureAlgorithm
}: {
@ -36,7 +36,7 @@ export const useUpdateOIDCConfig = () => {
clientSecret?: string;
isActive?: boolean;
configurationType?: string;
orgSlug: string;
organizationId: string;
manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
}) => {
@ -50,7 +50,7 @@ export const useUpdateOIDCConfig = () => {
tokenEndpoint,
userinfoEndpoint,
clientId,
orgSlug,
organizationId,
clientSecret,
isActive,
manageGroupMemberships,
@ -60,7 +60,7 @@ export const useUpdateOIDCConfig = () => {
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) });
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
}
});
@ -81,7 +81,7 @@ export const useCreateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug,
organizationId,
manageGroupMemberships,
jwtSignatureAlgorithm
}: {
@ -95,7 +95,7 @@ export const useCreateOIDCConfig = () => {
clientId: string;
clientSecret: string;
isActive: boolean;
orgSlug: string;
organizationId: string;
allowedEmailDomains?: string;
manageGroupMemberships?: boolean;
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
@ -112,7 +112,7 @@ export const useCreateOIDCConfig = () => {
clientId,
clientSecret,
isActive,
orgSlug,
organizationId,
manageGroupMemberships,
jwtSignatureAlgorithm
});
@ -120,7 +120,7 @@ export const useCreateOIDCConfig = () => {
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) });
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
}
});
};

View File

@ -5,18 +5,18 @@ import { apiRequest } from "@app/config/request";
import { OIDCConfigData } from "./types";
export const oidcConfigKeys = {
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const,
getOIDCConfig: (orgId: string) => [{ orgId }, "organization-oidc"] as const,
getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
["oidc-manage-group-memberships", orgId] as const
};
export const useGetOIDCConfig = (orgSlug: string) => {
export const useGetOIDCConfig = (orgId: string) => {
return useQuery({
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug),
queryKey: oidcConfigKeys.getOIDCConfig(orgId),
queryFn: async () => {
try {
const { data } = await apiRequest.get<OIDCConfigData>(
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
`/api/v1/sso/oidc/config?organizationId=${orgId}`
);
return data;

View File

@ -5,6 +5,7 @@ export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useResetPageHelper } from "./useResetPageHelper";
export * from "./useResizableHeaderHeight";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useTimedReset } from "./useTimedReset";
export { useToggle } from "./useToggle";

View File

@ -0,0 +1,71 @@
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
type Params = {
minHeight: number;
maxHeight: number;
initialHeight: number;
};
export const useResizableHeaderHeight = ({ minHeight, maxHeight, initialHeight }: Params) => {
const [headerHeight, setHeaderHeight] = useState(initialHeight);
const [isResizing, setIsResizing] = useState(false);
const startY = useRef(0);
const startHeight = useRef(0);
const handleMouseDown = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsResizing(true);
startY.current = e.clientY;
startHeight.current = headerHeight;
},
[headerHeight]
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isResizing) return;
const deltaY = e.clientY - startY.current;
const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight.current + deltaY));
setHeaderHeight(newHeight);
},
[isResizing]
);
const handleMouseUp = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
if (isResizing) {
document.addEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.addEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "ns-resize";
document.body.style.userSelect = "none";
}
return () => {
document.removeEventListener(
"mousemove",
// @ts-expect-error native discrepancy
handleMouseMove
);
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, handleMouseMove, handleMouseUp]);
return {
headerHeight,
handleMouseDown,
isResizing
};
};

View File

@ -29,6 +29,11 @@ const generalTabs = [
label: "Caching",
icon: "note",
link: "/admin/caching"
},
{
label: "Environment Variables",
icon: "unlock",
link: "/admin/environment"
}
];

View File

@ -113,7 +113,7 @@ export const ProjectSelect = () => {
<div>
<FontAwesomeIcon icon={faCube} className="text-xs" />
</div>
<Tooltip content={currentWorkspace.name}>
<Tooltip content={currentWorkspace.name} className="max-w-96">
<div className="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap">
{currentWorkspace?.name}
</div>
@ -176,7 +176,7 @@ export const ProjectSelect = () => {
>
<div className="flex items-center">
<div className="flex flex-1 items-center justify-between overflow-hidden">
<Tooltip content={workspace.name}>
<Tooltip side="right" className="break-words" content={workspace.name}>
<div className="max-w-40 overflow-hidden truncate whitespace-nowrap">
{workspace.name}
</div>

View File

@ -63,7 +63,7 @@ const router = createRouter({
context: { serverConfig: null, queryClient },
defaultPendingComponent: () => (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<Lottie icon="infisical_loading" className="h-32 w-32" />
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div>
),
defaultNotFoundComponent: NotFoundPage,

View File

@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { EnvironmentPageForm } from "./components";
export const EnvironmentPage = () => {
const { t } = useTranslation();
return (
<div className="bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Environment Variables"
description="Manage the environment variables for your Infisical instance."
/>
<EnvironmentPageForm />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,264 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Control, Controller, useForm, useWatch } from "react-hook-form";
import {
faArrowUpRightFromSquare,
faBookOpen,
faChevronRight,
faExclamationTriangle,
faMagnifyingGlass
} 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, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2";
import { HighlightText } from "@app/components/v2/HighlightText";
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
type TForm = Record<string, string>;
export const GroupContainer = ({
group,
control,
search
}: {
group: {
fields: {
key: string;
value: string;
hasEnvEntry: boolean;
description?: string;
}[];
name: string;
};
control: Control<TForm, any, TForm>;
search: string;
}) => {
const [open, setOpen] = useState(false);
return (
<div
key={group.name}
className="overflow-clip border border-b-0 border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md last:border-b"
>
<div
className="flex h-14 cursor-pointer items-center px-5 py-4 text-sm text-gray-300"
role="button"
tabIndex={0}
onClick={() => setOpen((o) => !o)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setOpen((o) => !o);
}
}}
>
<FontAwesomeIcon
className={`mr-8 transition-transform duration-100 ${open || search ? "rotate-90" : ""}`}
icon={faChevronRight}
/>
<div className="flex-grow select-none text-base">{group.name}</div>
</div>
{(open || search) && (
<div className="flex flex-col">
{group.fields.map((field) => (
<div
key={field.key}
className="flex items-center justify-between gap-4 border-t border-mineshaft-500 bg-mineshaft-700/50 p-4"
>
<div className="flex max-w-lg flex-col">
<span className="text-sm">
<HighlightText text={field.key} highlight={search} />
</span>
<span className="text-sm text-mineshaft-400">
<HighlightText text={field.description} highlight={search} />
</span>
</div>
<div className="flex grow items-center justify-end gap-2">
{field.hasEnvEntry && (
<Tooltip
content="Setting this value will override an existing environment variable"
className="text-center"
>
<FontAwesomeIcon icon={faExclamationTriangle} className="text-yellow" />
</Tooltip>
)}
<Controller
control={control}
name={field.key}
render={({ field: formGenField, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
className="mb-0 w-full max-w-sm"
>
<SecretInput
{...formGenField}
autoComplete="off"
containerClassName="text-bunker-300 hover:border-mineshaft-400 border border-mineshaft-600 bg-bunker-600 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
))}
</div>
)}
</div>
);
};
export const EnvironmentPageForm = () => {
const { data: envOverrides } = useGetEnvOverrides();
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
const [search, setSearch] = useState("");
const allFields = useMemo(() => {
if (!envOverrides) return [];
return Object.values(envOverrides).flatMap((group) => group.fields);
}, [envOverrides]);
const formSchema = useMemo(() => {
return z.object(Object.fromEntries(allFields.map((field) => [field.key, z.string()])));
}, [allFields]);
const defaultValues = useMemo(() => {
const values: Record<string, string> = {};
allFields.forEach((field) => {
values[field.key] = field.value ?? "";
});
return values;
}, [allFields]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, isDirty }
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues
});
const formValues = useWatch({ control });
const filteredData = useMemo(() => {
if (!envOverrides) return [];
const searchTerm = search.toLowerCase().trim();
if (!searchTerm) {
return Object.values(envOverrides);
}
return Object.values(envOverrides)
.map((group) => {
const filteredFields = group.fields.filter(
(field) =>
field.key.toLowerCase().includes(searchTerm) ||
(field.description ?? "").toLowerCase().includes(searchTerm)
);
if (filteredFields.length > 0) {
return { ...group, fields: filteredFields };
}
return null;
})
.filter(Boolean);
}, [search, formValues, envOverrides]);
useEffect(() => {
reset(defaultValues);
}, [defaultValues, reset]);
const onSubmit = useCallback(
async (formData: TForm) => {
try {
const filteredFormData = Object.fromEntries(
Object.entries(formData).filter(([, value]) => value !== "")
);
await updateServerConfig({
envOverrides: filteredFormData
});
createNotification({
type: "success",
text: "Environment overrides updated successfully. It can take up to 5 minutes to take effect."
});
reset(formData);
} catch (error) {
const errorMessage =
(error as any)?.response?.data?.message ||
(error as any)?.message ||
"An unknown error occurred";
createNotification({
type: "error",
title: "Failed to update environment overrides",
text: errorMessage
});
}
},
[reset, updateServerConfig]
);
return (
<form
className="flex flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex w-full flex-row items-center justify-between">
<div>
<div className="flex items-start gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Overrides</p>
<a
href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Override specific environment variables. After saving, it may take up to 5 minutes for
variables to propagate throughout every container.
</p>
</div>
<div className="flex flex-row gap-2">
<Button
type="submit"
variant="outline_bg"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</div>
</div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search for keys, descriptions, and values..."
className="flex-1"
/>
<div className="flex flex-col">
{filteredData.map((group) => (
<GroupContainer group={group!} control={control} search={search} />
))}
</div>
</form>
);
};

View File

@ -0,0 +1 @@
export { EnvironmentPageForm } from "./EnvironmentPageForm";

View File

@ -0,0 +1,25 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { EnvironmentPage } from "./EnvironmentPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/environment"
)({
component: EnvironmentPage,
beforeLoad: () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Environment",
link: linkOptions({
to: "/admin/environment"
})
}
]
};
}
});

View File

@ -1,19 +1,33 @@
import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
export const EmailNotVerifiedPage = () => {
return (
<div className="flex flex-col justify-between bg-bunker-800 md:h-screen">
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
<Helmet>
<title>Request a New Invite</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="flex h-screen w-screen flex-col items-center justify-center text-gray-200">
<p className="text-6xl">Oops.</p>
<p className="mb-1 mt-2 text-xl">Your email was not verified. </p>
<p className="text-xl">Please try again.</p>
<p className="text-md mt-8 max-w-sm text-center text-gray-600">
Note: If it still doesn&apos;t work, please reach out to us at support@infisical.com
<Link to="/">
<div className="mb-4 mt-20 flex justify-center">
<img src="/images/gradientLogo.svg" className="h-[90px] w-[120px]" alt="Infisical Logo" />
</div>
</Link>
<div className="mx-auto flex w-full flex-col items-center justify-center">
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Your email was not verified
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
Please try again. <br /> Note: If it still doesn&apos;t work, please reach out to us at
support@infisical.com
</p>
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link to="/login">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Back to Login
</span>
</Link>
</div>
</div>
</div>
);

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { Link, useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
@ -36,7 +36,12 @@ export const PasswordResetPage = () => {
const navigate = useNavigate();
return (
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
<Link to="/">
<div className="mb-4 mt-20 flex justify-center">
<img src="/images/gradientLogo.svg" className="h-[90px] w-[120px]" alt="Infisical Logo" />
</div>
</Link>
{step === Steps.ConfirmEmail && (
<ConfirmEmailStep
onComplete={(verifyToken, userEncryptionVersion) => {

View File

@ -19,17 +19,21 @@ export const ConfirmEmailStep = ({ onComplete }: Props) => {
isPending: isVerifyPasswordResetLoading
} = useVerifyPasswordResetCode();
return (
<div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6">
<p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent">
<div className="mx-auto flex w-full flex-col items-center justify-center">
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Confirm your email
</h1>
<p className="mb-8 w-max justify-center text-center text-sm text-gray-400">
Reset password for <span className="italic">{email}</span>.
</p>
<img
src="/images/envelope.svg"
style={{ height: "262px", width: "410px" }}
alt="verify email"
/>
<div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
onClick={async () => {
try {
const response = await verifyPasswordResetCodeMutateAsync({
@ -44,7 +48,6 @@ export const ConfirmEmailStep = ({ onComplete }: Props) => {
}
}}
isLoading={isVerifyPasswordResetLoading}
size="lg"
>
Confirm Email
</Button>

View File

@ -1,7 +1,7 @@
import crypto from "crypto";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router";
@ -169,17 +169,15 @@ export const EnterPasswordStep = ({
return (
<form
onSubmit={handleSubmit(resetPasswordHandler)}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
className="mx-auto flex w-full flex-col items-center justify-center"
>
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Enter new password
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
Make sure you save it somewhere safe.
</p>
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
Make sure you save it somewhere safe.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Controller
control={control}
name="password"
@ -202,6 +200,20 @@ export const EnterPasswordStep = ({
)}
/>
</div>
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
isLoading={isSubmitting || isLoading || isLoadingV2}
isDisabled={isSubmitting || isLoading || isLoadingV2}
>
Change Password
</Button>
</div>
{passwordErrorTooShort ||
passwordErrorTooLong ||
passwordErrorNoLetterChar ||
@ -210,33 +222,33 @@ export const EnterPasswordStep = ({
passwordErrorEscapeChar ||
passwordErrorLowEntropy ||
passwordErrorBreached ? (
<div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">Password should contain:</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="mt-4 rounded border border-mineshaft-600 bg-mineshaft-800 p-4 drop-shadow">
<div className="mb-1 ml-2 text-sm text-gray-300">Password should contain:</div>
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorTooShort ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
at least 14 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorTooLong ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
at most 100 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorNoLetterChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -244,11 +256,11 @@ export const EnterPasswordStep = ({
at least 1 letter character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorNoNumOrSpecialChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div
className={`${
@ -258,11 +270,11 @@ export const EnterPasswordStep = ({
at least 1 number or special character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorRepeatedChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -270,11 +282,11 @@ export const EnterPasswordStep = ({
at most 3 repeated, consecutive characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorEscapeChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -282,11 +294,11 @@ export const EnterPasswordStep = ({
No escape characters allowed.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorLowEntropy ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
@ -294,32 +306,18 @@ export const EnterPasswordStep = ({
Password contains personal info.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
<div className="ml-2 flex flex-row items-center justify-start">
{passwordErrorBreached ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
)}
<div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
Password was found in a data breach.
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isLoading || isLoadingV2}
isDisabled={isSubmitting || isLoading || isLoadingV2}
>
Change Password
</Button>
</div>
</div>
) : null}
</form>
);
};

View File

@ -43,18 +43,15 @@ export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) =>
return (
<form
onSubmit={handleSubmit(getEncryptedKeyHandler)}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
className="mx-auto flex w-full flex-col items-center justify-center"
>
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100">
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Enter your backup key
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
You can find it in your emergency kit you downloaded during signup.
</p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-full px-4 text-center text-sm text-gray-400 sm:max-w-md">
You can find it in your emergency kit. You had to download the emergency kit during
signup.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Controller
control={control}
name="backupKey"
@ -75,12 +72,17 @@ export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) =>
)}
/>
</div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button type="submit" colorSchema="secondary">
Submit Backup Key
</Button>
</div>
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
>
Submit Backup Key
</Button>
</div>
</form>
);

View File

@ -2,8 +2,7 @@ import { FormEvent, useState } from "react";
import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
import InputField from "@app/components/basic/InputField";
import { Button, EmailServiceSetupModal } from "@app/components/v2";
import { Button, EmailServiceSetupModal, Input } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { useSendPasswordResetEmail } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
@ -44,9 +43,9 @@ export const VerifyEmailPage = () => {
};
return (
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6">
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
<Helmet>
<title>Login</title>
<title>Reset Password</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Verify your email in Infisical" />
@ -56,66 +55,80 @@ export const VerifyEmailPage = () => {
/>
</Helmet>
<Link to="/">
<div className="mb-8 mt-20 flex cursor-pointer justify-center">
<div className="mb-4 mt-20 flex justify-center">
<img
src="/images/biglogo.png"
src="/images/gradientLogo.svg"
style={{
height: "90px",
width: "120px"
}}
alt="long logo"
alt="Infisical Logo"
/>
</div>
</Link>
{step === 1 && (
<form
onSubmit={onSubmit}
className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl"
className="mx-auto flex w-full flex-col items-center justify-center"
>
<p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Forgot your password?
</h1>
<p className="w-max justify-center text-center text-sm text-gray-400">
Enter your email to start the password reset process. <br /> You will receive an email
with instructions.
</p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max justify-center text-center text-sm text-gray-400">
Enter your email to start the password reset process. You will receive an email with
instructions.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<InputField
label="Email"
onChangeHandler={setEmail}
type="email"
<div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={email}
placeholder=""
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-10"
/>
</div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button type="submit" size="lg" onClick={() => {}} isLoading={loading}>
Continue
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
isLoading={loading}
>
Continue
</Button>
</div>
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link to="/login">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Back to Login
</span>
</Link>
</div>
</form>
)}
{step === 2 && (
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-semibold text-bunker-100 md:text-2xl">
Look for an email in your inbox.
<div className="mx-auto flex w-full flex-col items-center justify-center">
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Look for an email in your inbox
</h1>
<p className="w-max max-w-lg justify-center text-center text-sm text-gray-400">
If the email is in our system, you will receive an email at{" "}
<span className="italic">{email}</span> with instructions on how to reset your password.
</p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="w-max text-center text-sm text-gray-400">
If the email is in our system, you will receive an email at{" "}
<span className="italic">{email}</span> with instructions on how to reset your
password.
</p>
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link to="/login">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Back to Login
</span>
</Link>
</div>
</div>
)}
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}

View File

@ -0,0 +1,212 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import ms from "ms";
import { twMerge } from "tailwind-merge";
import {
Button,
DatePicker,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import {
auditLogDateFilterFormSchema,
AuditLogDateFilterType,
TAuditLogDateFilterFormData
} from "./types";
type Props = {
setFilter: (data: TAuditLogDateFilterFormData) => void;
filter: TAuditLogDateFilterFormData;
};
const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"];
export const LogsDateFilter = ({ setFilter, filter }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const [isPopupOpen, setIsPopOpen] = useState(false);
const { control, watch, handleSubmit, formState } = useForm<TAuditLogDateFilterFormData>({
resolver: zodResolver(auditLogDateFilterFormSchema),
values: filter
});
const selectType = watch("type");
const isCustomRelative =
filter.type === AuditLogDateFilterType.Relative &&
!RELATIVE_VALUES.includes(filter.relativeModeValue || "");
const onSubmit = (data: TAuditLogDateFilterFormData) => {
const endDate = data.type === AuditLogDateFilterType.Relative ? new Date() : data.endDate;
const startDate =
data.type === AuditLogDateFilterType.Relative && data.relativeModeValue
? new Date(Number(new Date()) - ms(data.relativeModeValue))
: data.startDate;
setFilter({
...data,
startDate,
endDate
});
setIsPopOpen(false);
};
return (
<DropdownMenu open={isPopupOpen} onOpenChange={(el) => setIsPopOpen(el)}>
<div className="mr-2 flex items-center">
{filter.type === AuditLogDateFilterType.Relative ? (
<>
{RELATIVE_VALUES.map((el) => (
<Button
variant="outline_bg"
className={twMerge(
"rounded-none px-3 py-2 first:rounded-l-md",
filter.type === AuditLogDateFilterType.Relative &&
filter.relativeModeValue === el &&
"border-primary/40 bg-primary/[0.1]"
)}
key={`${el}-relative`}
onClick={() =>
setFilter({
relativeModeValue: el,
type: AuditLogDateFilterType.Relative,
endDate: new Date(),
startDate: new Date(Number(new Date()) - ms(el))
})
}
>
{el}
</Button>
))}
</>
) : (
<>
<div className="rounded-l-md border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
{format(filter.startDate, "yyyy-MM-dd HH:mm")}
</div>
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
{format(filter.endDate, "yyyy-MM-dd HH:mm")}
</div>
</>
)}
<DropdownMenuTrigger asChild>
<Button
variant="outline_bg"
className={twMerge(
"rounded-none rounded-r-md px-3 py-2",
(filter.type === AuditLogDateFilterType.Absolute || isCustomRelative) &&
"border-primary/40 bg-primary/[0.1]"
)}
>
<FontAwesomeIcon icon={faCalendar} />
{filter.type === AuditLogDateFilterType.Relative && isCustomRelative && (
<span className="ml-1">({filter.relativeModeValue})</span>
)}
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent className="min-w-80 p-4" align="center" alignOffset={16}>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="type"
render={({ field }) => (
<FormControl className="mb-2 w-full">
<Select
value={field.value}
onValueChange={(el) => field.onChange(el)}
className="w-full"
>
<SelectItem value={AuditLogDateFilterType.Relative}>Relative</SelectItem>
<SelectItem value={AuditLogDateFilterType.Absolute}>Absolute</SelectItem>
</Select>
</FormControl>
)}
/>
{selectType === AuditLogDateFilterType.Relative && (
<Controller
control={control}
name="relativeModeValue"
render={({ field }) => (
<FormControl className="mb-0 w-full" helperText="Example: 1h, 1d, 2d">
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
)}
{selectType === AuditLogDateFilterType.Absolute && (
<div className="mb-2 flex h-10 w-full items-center justify-between gap-2">
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<div className="flex items-center -space-x-3">
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
</div>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
)}
<div className="mt-4">
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
Apply
</Button>
</div>
</form>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@ -1,28 +1,15 @@
/* eslint-disable no-nested-ternary */
import { useMemo, useState } from "react";
import {
Control,
Controller,
UseFormGetFieldState,
UseFormReset,
UseFormResetField,
UseFormSetValue,
UseFormWatch
} from "react-hook-form";
import {
faArrowRight,
faCaretDown,
faCheckCircle,
faFilterCircleXmark
} from "@fortawesome/free-solid-svg-icons";
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import {
Badge,
Button,
DatePicker,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@ -44,7 +31,7 @@ import { EventType } from "@app/hooks/api/auditLogs/enums";
import { UserAgentType } from "@app/hooks/api/auth/types";
import { LogFilterItem } from "./LogFilterItem";
import { AuditLogFilterFormData, Presets } from "./types";
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label]) => ({
@ -54,78 +41,54 @@ const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label
type Props = {
presets?: Presets;
control: Control<AuditLogFilterFormData>;
reset: UseFormReset<AuditLogFilterFormData>;
resetField: UseFormResetField<AuditLogFilterFormData>;
watch: UseFormWatch<AuditLogFilterFormData>;
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>;
setValue: UseFormSetValue<AuditLogFilterFormData>;
setFilter: (data: TAuditLogFilterFormData) => void;
filter: TAuditLogFilterFormData;
};
const getActiveFilterCount = (
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>,
watch: UseFormWatch<AuditLogFilterFormData>
) => {
const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
const fields = [
"actor",
"project",
"eventType",
"startDate",
"endDate",
"environment",
"secretPath",
"userAgentType",
"secretKey"
] as Partial<keyof AuditLogFilterFormData>[];
] as Partial<keyof TAuditLogFilterFormData>[];
let filterCount = 0;
// either start or end date should only be counted as one filter
let dateProcessed = false;
fields.forEach((field) => {
const fieldState = getFieldState(field);
if (
field === "userAgentType" ||
field === "environment" ||
field === "secretKey" ||
field === "secretPath"
) {
const value = watch(field);
if (value !== undefined && value !== "") {
filterCount += 1;
}
} else if (fieldState.isDirty && !dateProcessed) {
const value = filter?.[field];
if (Array.isArray(value) ? value.length : value) {
filterCount += 1;
if (field === "startDate" || field === "endDate") {
dateProcessed = true;
}
}
});
return filterCount;
};
export const LogsFilter = ({
presets,
control,
reset,
resetField,
watch,
getFieldState,
setValue
}: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
const { data: workspaces = [] } = useGetUserWorkspaces();
const { currentOrg } = useOrganization();
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
const { control, watch, resetField, setValue, handleSubmit, formState } =
useForm<TAuditLogFilterFormData>({
resolver: zodResolver(auditLogFilterFormSchema),
defaultValues: {
project: null,
environment: undefined,
secretKey: "",
secretPath: "",
actor: presets?.actorId,
eventType: filter?.eventType || [],
userAgentType: undefined
},
values: filter
});
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
const selectedProject = watch("project");
@ -140,408 +103,345 @@ export const LogsFilter = ({
return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? [];
}, [selectedProject, workspacesInOrg]);
const activeFilterCount = getActiveFilterCount(getFieldState, watch);
const activeFilterCount = getActiveFilterCount(filter);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline_bg" colorSchema="primary">
<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-3 px-[0.1rem]" />
Filters
<Button variant="outline_bg" colorSchema="primary" className="relative">
<FontAwesomeIcon icon={faFilterCircleXmark} />
{activeFilterCount > 0 && (
<Badge className="ml-2 px-1.5 py-0.5" variant="primary">
<Badge className="absolute bottom-0 right-0" variant="primary">
{activeFilterCount}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="mt-4 py-4">
<div className="flex min-w-64 flex-col font-inter">
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
<span>Filters</span>
<Badge className="px-1.5 py-0.5" variant="primary">
{activeFilterCount}
</Badge>
<form onSubmit={handleSubmit(setFilter)}>
<div className="flex min-w-64 flex-col font-inter">
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2">
<span>Filters</span>
<Badge className="px-1.5 py-0.5" variant="primary">
{activeFilterCount}
</Badge>
</div>
<Button
onClick={() => {
setFilter({
eventType: presets?.eventType || [],
actor: presets?.actorId,
userAgentType: undefined,
project: null,
secretPath: undefined,
secretKey: undefined
});
}}
variant="link"
className="text-mineshaft-400"
size="xs"
>
Clear filters
</Button>
</div>
<Button
onClick={() => {
reset({
eventType: presets?.eventType || [],
actor: presets?.actorId,
userAgentType: undefined,
startDate: undefined,
endDate: undefined,
project: null,
secretPath: undefined,
secretKey: undefined
});
</div>
<div className="px-3">
<LogFilterItem
label="Events"
onClear={() => {
resetField("eventType");
}}
variant="link"
className="text-mineshaft-400"
size="xs"
>
Clear filters
<Controller
control={control}
name="eventType"
render={({ field }) => (
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{selectedEventTypes?.length === 1
? eventTypes.find(
(eventType) => eventType.value === selectedEventTypes[0]
)?.label
: selectedEventTypes?.length === 0
? "All events"
: `${selectedEventTypes?.length} events selected`}
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
>
<div className="max-h-80 overflow-y-auto">
{eventTypes && eventTypes.length > 0 ? (
eventTypes.map((eventType) => {
const isSelected = selectedEventTypes?.includes(
eventType.value as EventType
);
return (
<DropdownMenuItem
onSelect={(event) =>
eventTypes.length > 1 && event.preventDefault()
}
onClick={() => {
if (
selectedEventTypes?.includes(eventType.value as EventType)
) {
field.onChange(
selectedEventTypes?.filter(
(e: string) => e !== eventType.value
)
);
} else {
field.onChange([
...(selectedEventTypes || []),
eventType.value
]);
}
}}
key={`event-type-${eventType.value}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{eventType.label}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Source"
onClear={() => {
resetField("userAgentType");
}}
>
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-full"
>
<Select
{...field}
value={value === undefined ? "all" : value}
onValueChange={(e) => {
if (e === "all") onChange(undefined);
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
}}
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
>
<SelectItem value="all" key="all">
All sources
</SelectItem>
{userAgentTypes.map(({ label, value: userAgent }) => (
<SelectItem value={userAgent} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Project"
onClear={() => {
resetField("project");
resetField("environment");
setValue("secretPath", "");
setValue("secretKey", "");
}}
>
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="mb-0 w-full"
>
<FilterableSelect
value={value}
isClearable
onChange={(e) => {
if (e === null) {
setValue("secretPath", "");
setValue("secretKey", "");
}
resetField("environment");
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
name,
id,
type: defaultProduct
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</LogFilterItem>
<AnimatePresence initial={false}>
{showSecretsSection && (
<motion.div
className="overflow-hidden"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<div className="mb-3 mt-2">
<p className="text-xs opacity-60">Secrets</p>
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
</div>
<LogFilterItem
label="Environment"
hoverTooltip={
!selectedProject
? "Select a project before filtering by environment."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
resetField("environment");
}}
>
<Controller
control={control}
name="environment"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-full"
>
<FilterableSelect
value={value}
key={value?.name || "filter-environment"}
isClearable
isDisabled={!selectedProject}
onChange={(e) => onChange(e)}
placeholder="All environments"
options={availableEnvironments.map(({ name, slug }) => ({
name,
slug
}))}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Secret Path"
hoverTooltip={
!selectedProject
? "Select a project before filtering by secret path."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
setValue("secretPath", "");
}}
>
<Controller
control={control}
name="secretPath"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
className="w-full"
>
<Input
placeholder="Enter secret path"
className="disabled:cursor-not-allowed"
isDisabled={!selectedProject}
{...field}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
hoverTooltip={
!selectedProject
? "Select a project before filtering by secret key."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
label="Secret Key"
onClear={() => {
setValue("secretKey", "");
}}
>
<Controller
control={control}
name="secretKey"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to a specific secret."
className="w-full"
>
<Input
isDisabled={!selectedProject}
{...field}
placeholder="Enter secret key"
className="disabled:cursor-not-allowed"
value={value}
onChange={(e) =>
setValue("secretKey", e.target.value, { shouldDirty: true })
}
/>
</FormControl>
)}
/>
</LogFilterItem>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="mt-2 px-3">
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
Apply
</Button>
</div>
</div>
<div className="px-3">
<LogFilterItem
label="Events"
onClear={() => {
resetField("eventType");
}}
>
<Controller
control={control}
name="eventType"
render={({ field }) => (
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{selectedEventTypes?.length === 1
? eventTypes.find(
(eventType) => eventType.value === selectedEventTypes[0]
)?.label
: selectedEventTypes?.length === 0
? "All events"
: `${selectedEventTypes?.length} events selected`}
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
>
<div className="max-h-80 overflow-y-auto">
{eventTypes && eventTypes.length > 0 ? (
eventTypes.map((eventType) => {
const isSelected = selectedEventTypes?.includes(
eventType.value as EventType
);
return (
<DropdownMenuItem
onSelect={(event) =>
eventTypes.length > 1 && event.preventDefault()
}
onClick={() => {
if (
selectedEventTypes?.includes(eventType.value as EventType)
) {
field.onChange(
selectedEventTypes?.filter(
(e: string) => e !== eventType.value
)
);
} else {
field.onChange([
...(selectedEventTypes || []),
eventType.value
]);
}
}}
key={`event-type-${eventType.value}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{eventType.label}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Source"
onClear={() => {
resetField("userAgentType");
}}
>
<Controller
control={control}
name="userAgentType"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-full"
>
<Select
value={value === undefined ? "all" : value}
{...field}
onValueChange={(e) => {
if (e === "all") onChange(undefined);
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
}}
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
>
<SelectItem value="all" key="all">
All sources
</SelectItem>
{userAgentTypes.map(({ label, value: userAgent }) => (
<SelectItem value={userAgent} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Date"
onClear={() => {
resetField("startDate");
resetField("endDate");
}}
>
<div className="flex h-10 w-full items-center justify-between gap-2">
<Controller
name="startDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
<div className="flex items-center -space-x-3">
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
</div>
<Controller
name="endDate"
control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
className="relative top-2"
errorText={error?.message}
isError={Boolean(error)}
>
<DatePicker
value={field.value || undefined}
onChange={onChange}
dateFormat="P"
popUpProps={{
open: isEndDatePickerOpen,
onOpenChange: setIsEndDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/>
</div>
</LogFilterItem>
<AnimatePresence initial={false}>
{showSecretsSection && (
<motion.div
className="mt-2 overflow-hidden"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<div className="mb-3 mt-2">
<p className="text-xs opacity-60">Secrets</p>
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
</div>
<LogFilterItem
label="Project"
onClear={() => {
resetField("project");
resetField("environment");
setValue("secretPath", "");
setValue("secretKey", "");
}}
>
<Controller
control={control}
name="project"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-full"
>
<FilterableSelect
value={value}
isClearable
onChange={(e) => {
if (e === null) {
setValue("secretPath", "");
setValue("secretKey", "");
}
resetField("environment");
onChange(e);
}}
placeholder="All projects"
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
name,
id,
type: defaultProduct
}))}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Environment"
hoverTooltip={
!selectedProject
? "Select a project before filtering by environment."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
resetField("environment");
}}
>
<Controller
control={control}
name="environment"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-full"
>
<FilterableSelect
value={value}
key={value?.name || "filter-environment"}
isClearable
isDisabled={!selectedProject}
onChange={(e) => onChange(e)}
placeholder="All environments"
options={availableEnvironments.map(({ name, slug }) => ({
name,
slug
}))}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
label="Secret Path"
hoverTooltip={
!selectedProject
? "Select a project before filtering by secret path."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
onClear={() => {
setValue("secretPath", "");
}}
>
<Controller
control={control}
name="secretPath"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
className="w-full"
>
<Input
placeholder="Enter secret path"
className="disabled:cursor-not-allowed"
isDisabled={!selectedProject}
{...field}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</LogFilterItem>
<LogFilterItem
hoverTooltip={
!selectedProject
? "Select a project before filtering by secret key."
: undefined
}
className={twMerge(!selectedProject && "opacity-50")}
label="Secret Key"
onClear={() => {
setValue("secretKey", "");
}}
>
<Controller
control={control}
name="secretKey"
render={({ field: { onChange, value, ...field } }) => (
<FormControl
tooltipText="Filter audit logs related to a specific secret."
className="w-full"
>
<Input
isDisabled={!selectedProject}
{...field}
placeholder="Enter secret key"
className="disabled:cursor-not-allowed"
value={value}
onChange={(e) =>
setValue("secretKey", e.target.value, { shouldDirty: true })
}
/>
</FormControl>
)}
/>
</LogFilterItem>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</form>
</DropdownMenuContent>
</DropdownMenu>
);

View File

@ -1,17 +1,20 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import ms from "ms";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { usePopUp } from "@app/hooks/usePopUp";
import { LogsDateFilter } from "./LogsDateFilter";
import { LogsFilter } from "./LogsFilter";
import { LogsTable } from "./LogsTable";
import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types";
import {
AuditLogDateFilterType,
Presets,
TAuditLogDateFilterFormData,
TAuditLogFilterFormData
} from "./types";
type Props = {
presets?: Presets;
@ -24,74 +27,45 @@ export const LogsSection = withPermission(
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
const { control, reset, watch, getFieldState, resetField, setValue } =
useForm<AuditLogFilterFormData>({
resolver: zodResolver(auditLogFilterFormSchema),
defaultValues: {
project: null,
environment: undefined,
secretKey: "",
secretPath: "",
actor: presets?.actorId,
eventType: presets?.eventType || [],
userAgentType: undefined,
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)),
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999))
}
});
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
eventType: presets?.eventType || [],
actor: presets?.actorId
});
const [dateFilter, setDateFilter] = useState<TAuditLogDateFilterFormData>({
startDate: new Date(Number(new Date()) - ms("1h")),
endDate: new Date(),
type: AuditLogDateFilterType.Relative,
relativeModeValue: "1h"
});
useEffect(() => {
if (subscription && !subscription.auditLogs) {
handlePopUpOpen("upgradePlan");
}
}, [subscription]);
const eventType = watch("eventType") as EventType[] | undefined;
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor");
const projectId = watch("project")?.id;
const environment = watch("environment")?.slug;
const secretPath = watch("secretPath");
const secretKey = watch("secretKey");
const startDate = watch("startDate");
const endDate = watch("endDate");
const [debouncedSecretPath] = useDebounce<string>(secretPath!, 500);
const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500);
return (
<div className="space-y-2">
<div className="flex w-full justify-end">
{showFilters && <LogsDateFilter filter={dateFilter} setFilter={setDateFilter} />}
{showFilters && (
<LogsFilter
presets={presets}
control={control}
watch={watch}
reset={reset}
resetField={resetField}
getFieldState={getFieldState}
setValue={setValue}
/>
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
)}
</div>
<LogsTable
refetchInterval={refetchInterval}
filter={{
secretPath: debouncedSecretPath || undefined,
secretKey: debouncedSecretKey || undefined,
eventMetadata: presets?.eventMetadata,
projectId,
secretPath: logFilter.secretPath || undefined,
secretKey: logFilter.secretKey || undefined,
eventMetadata: logFilter?.eventMetadata,
projectId: logFilter?.project?.id,
actorType: presets?.actorType,
limit: 15,
eventType,
userAgentType,
startDate,
endDate,
environment,
actor
eventType: logFilter?.eventType,
userAgentType: logFilter?.userAgentType,
startDate: dateFilter?.startDate,
endDate: dateFilter?.endDate,
environment: logFilter?.environment?.slug,
actor: logFilter?.actor
}}
/>
<UpgradePlanModal

View File

@ -23,11 +23,11 @@ import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
import { LogsTableRow } from "./LogsTableRow";
type Props = {
filter?: TGetAuditLogsFilter;
filter: TGetAuditLogsFilter;
refetchInterval?: number;
};
const AUDIT_LOG_LIMIT = 15;
const AUDIT_LOG_LIMIT = 30;
export const LogsTable = ({ filter, refetchInterval }: Props) => {
// Determine the project ID for filtering

View File

@ -3,23 +3,33 @@ import { z } from "zod";
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const auditLogFilterFormSchema = z
export enum AuditLogDateFilterType {
Relative = "relative",
Absolute = "absolute"
}
export const auditLogFilterFormSchema = z.object({
eventMetadata: z.object({}).optional(),
project: z
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
.optional()
.nullable(),
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
eventType: z.nativeEnum(EventType).array(),
actor: z.string().optional(),
userAgentType: z.nativeEnum(UserAgentType).optional(),
secretPath: z.string().optional(),
secretKey: z.string().optional(),
page: z.coerce.number().optional(),
perPage: z.coerce.number().optional()
});
export const auditLogDateFilterFormSchema = z
.object({
eventMetadata: z.object({}).optional(),
project: z
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
.optional()
.nullable(),
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
eventType: z.nativeEnum(EventType).array(),
actor: z.string().optional(),
userAgentType: z.nativeEnum(UserAgentType),
secretPath: z.string().optional(),
secretKey: z.string().optional(),
startDate: z.date().optional(),
endDate: z.date().optional(),
page: z.coerce.number().optional(),
perPage: z.coerce.number().optional()
type: z.nativeEnum(AuditLogDateFilterType),
relativeModeValue: z.string().optional(),
startDate: z.date(),
endDate: z.date()
})
.superRefine((el, ctx) => {
if (el.endDate && el.startDate && el.endDate < el.startDate) {
@ -31,10 +41,11 @@ export const auditLogFilterFormSchema = z
}
});
export type AuditLogFilterFormData = z.infer<typeof auditLogFilterFormSchema>;
export type TAuditLogFilterFormData = z.infer<typeof auditLogFilterFormSchema>;
export type TAuditLogDateFilterFormData = z.infer<typeof auditLogDateFilterFormSchema>;
export type SetValueType = (
name: keyof AuditLogFilterFormData,
name: keyof TAuditLogFilterFormData,
value: any,
options?: {
shouldValidate?: boolean;

View File

@ -105,7 +105,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateOIDCConfig();
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false);
const { data } = useGetOIDCConfig(currentOrg?.slug ?? "");
const { data } = useGetOIDCConfig(currentOrg?.id ?? "");
const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({
resolver: zodResolver(schema),
@ -134,7 +134,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId: "",
clientSecret: "",
isActive: false,
orgSlug: currentOrg.slug
organizationId: currentOrg.id
});
createNotification({
@ -196,7 +196,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId,
clientSecret,
isActive: true,
orgSlug: currentOrg.slug,
organizationId: currentOrg.id,
jwtSignatureAlgorithm
});
} else {
@ -212,7 +212,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
clientId,
clientSecret,
isActive: true,
orgSlug: currentOrg.slug,
organizationId: currentOrg.id,
jwtSignatureAlgorithm
});
}

View File

@ -21,7 +21,7 @@ export const OrgOIDCSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { data, isPending } = useGetOIDCConfig(currentOrg?.slug ?? "");
const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateOIDCConfig();
const { mutateAsync: updateOrg } = useUpdateOrg();
@ -41,7 +41,7 @@ export const OrgOIDCSection = (): JSX.Element => {
}
await mutateAsync({
orgSlug: currentOrg?.slug,
organizationId: currentOrg?.id,
isActive: value
});
@ -114,7 +114,7 @@ export const OrgOIDCSection = (): JSX.Element => {
}
await mutateAsync({
orgSlug: currentOrg?.slug,
organizationId: currentOrg?.id,
manageGroupMemberships: value
});

View File

@ -38,7 +38,7 @@ export const OrgSsoTab = withPermission(
const { subscription } = useSubscription();
const { data: oidcConfig, isPending: isLoadingOidcConfig } = useGetOIDCConfig(
currentOrg?.slug ?? ""
currentOrg?.id ?? ""
);
const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig(
currentOrg?.id ?? ""

View File

@ -1,41 +1,37 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from '@app/components/v2'
import { workspaceKeys } from '@app/hooks/api'
import {
fetchUserProjectPermissions,
roleQueryKeys,
} from '@app/hooks/api/roles/queries'
import { fetchWorkspaceById } from '@app/hooks/api/workspace/queries'
import { ProjectLayout } from '@app/layouts/ProjectLayout'
import { ProjectSelect } from '@app/layouts/ProjectLayout/components/ProjectSelect'
import { BreadcrumbTypes } from "@app/components/v2";
import { workspaceKeys } from "@app/hooks/api";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { ProjectLayout } from "@app/layouts/ProjectLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout',
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout"
)({
component: ProjectLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId),
})
queryFn: () => fetchWorkspaceById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId,
workspaceId: params.projectId
}),
queryFn: () =>
fetchUserProjectPermissions({ workspaceId: params.projectId }),
})
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
});
return {
project,
breadcrumbs: [
{
type: BreadcrumbTypes.Component,
component: ProjectSelect,
},
],
}
},
})
component: ProjectSelect
}
]
};
}
});

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
@ -58,6 +58,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { HeaderResizer } from "@app/components/v2/HeaderResizer/HeaderResizer";
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
@ -73,7 +74,14 @@ import {
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper, useToggle } from "@app/hooks";
import {
useDebounce,
usePagination,
usePopUp,
useResetPageHelper,
useResizableHeaderHeight,
useToggle
} from "@app/hooks";
import {
useCreateFolder,
useCreateSecretV3,
@ -97,6 +105,7 @@ import {
useSecretRotationOverview
} from "@app/hooks/utils";
import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/OverviewPage/components/SecretOverviewSecretRotationRow";
import { getHeaderStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
@ -142,6 +151,8 @@ const DEFAULT_FILTER_STATE = {
[RowType.SecretRotation]: true
};
const DEFAULT_COLLAPSED_HEADER_HEIGHT = 120;
export const OverviewPage = () => {
const { t } = useTranslation();
@ -159,7 +170,7 @@ export const OverviewPage = () => {
const [scrollOffset, setScrollOffset] = useState(0);
const [debouncedScrollOffset] = useDebounce(scrollOffset);
const { permission } = useProjectPermission();
const tableRef = useRef<HTMLDivElement>(null);
const { currentWorkspace } = useWorkspace();
const isProjectV3 = currentWorkspace?.version === ProjectVersion.V3;
const workspaceId = currentWorkspace?.id as string;
@ -861,10 +872,26 @@ export const OverviewPage = () => {
);
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
const storedHeight = Number.parseInt(
localStorage.getItem("overview-header-height") ?? DEFAULT_COLLAPSED_HEADER_HEIGHT.toString(),
10
);
const { headerHeight, handleMouseDown, isResizing } = useResizableHeaderHeight({
initialHeight: Number.isNaN(storedHeight) ? DEFAULT_COLLAPSED_HEADER_HEIGHT : storedHeight,
minHeight: DEFAULT_COLLAPSED_HEADER_HEIGHT,
maxHeight: 288
});
const debouncedHeaderHeight = useDebounce(headerHeight);
useEffect(() => {
localStorage.setItem("overview-header-height", debouncedHeaderHeight.toString());
}, [debouncedHeaderHeight]);
if (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]">
<Lottie icon="infisical_loading" className="h-32 w-32" />
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div>
);
}
@ -892,7 +919,7 @@ export const OverviewPage = () => {
<meta property="og:title" content={String(t("dashboard.og-title"))} />
<meta name="og:description" content={String(t("dashboard.og-description"))} />
</Helmet>
<div className="mx-auto max-w-7xl text-mineshaft-50 dark:[color-scheme:dark]">
<div className="relative mx-auto max-w-7xl text-mineshaft-50 dark:[color-scheme:dark]">
<div className="flex w-full items-baseline justify-between">
<PageHeader
title="Secrets Overview"
@ -959,7 +986,10 @@ export const OverviewPage = () => {
</Tooltip>
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
align="end"
>
{/* <DropdownMenuItem className="px-1.5" asChild>
<Button
size="xs"
@ -1176,21 +1206,20 @@ export const OverviewPage = () => {
secretsToDeleteKeys={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncs}
/>
<div className="thin-scrollbar mt-4">
<div ref={tableRef} className="thin-scrollbar mt-4">
<TableContainer
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
className="thin-scrollbar rounded-b-none"
>
<Table>
<THead className={collapseEnvironments ? "h-24" : ""}>
<THead style={{ height: collapseEnvironments ? headerHeight : undefined }}>
<Tr
className={twMerge("sticky top-0 z-20 border-0", collapseEnvironments && "h-24")}
className="sticky top-0 z-20 border-0"
style={{ height: collapseEnvironments ? headerHeight : undefined }}
>
<Th
className={twMerge(
"sticky left-0 z-20 min-w-[20rem] border-b-0 p-0",
collapseEnvironments && "h-24"
)}
className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0"
style={{ height: collapseEnvironments ? headerHeight : undefined }}
>
<div
className={twMerge(
@ -1264,13 +1293,23 @@ export const OverviewPage = () => {
const importedSecKeyCount = getEnvImportedSecretKeyCount(slug);
const missingKeyCount = secKeys.length - envSecKeyCount - importedSecKeyCount;
const isLast = index === visibleEnvs.length - 1;
return (
<Th
className={twMerge(
"min-table-row border-b-0 p-0 text-xs",
collapseEnvironments && index === visibleEnvs.length - 1 && "mr-8",
collapseEnvironments ? "h-24 w-[1rem]" : "min-w-[11rem] text-center"
collapseEnvironments && index === visibleEnvs.length - 1 && "!mr-8",
!collapseEnvironments && "min-w-[11rem] text-center"
)}
style={
collapseEnvironments
? {
height: headerHeight,
width: "w-[1rem]"
}
: undefined
}
key={`secret-overview-${name}-${index + 1}`}
>
<Tooltip
@ -1293,33 +1332,41 @@ export const OverviewPage = () => {
className={twMerge(
"border-b border-mineshaft-600",
collapseEnvironments
? "relative h-24 w-[2.9rem]"
? "relative"
: "flex items-center justify-center px-5 pb-[0.82rem] pt-3.5",
collapseEnvironments &&
index === visibleEnvs.length - 1 &&
"overflow-clip"
collapseEnvironments && isLast && "overflow-clip"
)}
style={{
height: collapseEnvironments ? headerHeight : undefined,
minWidth: collapseEnvironments ? "2.9rem" : undefined,
width: collapseEnvironments && isLast ? headerHeight * 0.3 : undefined
}}
>
<div
className={twMerge(
"border-mineshaft-600",
collapseEnvironments
? "ml-[0.85rem] h-24 -skew-x-[16rad] transform border-l text-xs"
? "-skew-x-[16rad] transform border-l text-xs"
: "flex items-center justify-center"
)}
style={{
height: collapseEnvironments ? headerHeight : undefined,
marginLeft: collapseEnvironments ? headerHeight * 0.145 : undefined
}}
/>
<button
type="button"
className={twMerge(
"duration-100 hover:text-mineshaft-100",
collapseEnvironments &&
(index === visibleEnvs.length - 1
? "bottom-[1.75rem] w-14"
: "bottom-10 w-20"),
collapseEnvironments
? "absolute -rotate-[72.25deg] text-left !text-[12px] font-normal"
? "absolute -rotate-[72.75deg] text-left text-sm font-normal"
: "flex items-center text-center text-sm font-medium"
)}
style={getHeaderStyle({
collapseEnvironments,
isLast,
headerHeight
})}
onClick={() => handleExploreEnvClick(slug)}
>
<p className="truncate font-medium">{name}</p>
@ -1340,6 +1387,14 @@ export const OverviewPage = () => {
);
})}
</Tr>
{collapseEnvironments && (
<HeaderResizer
onMouseDown={handleMouseDown}
isActive={isResizing}
scrollOffset={scrollOffset}
heightOffset={(tableRef.current?.clientTop ?? 0) + headerHeight - 2.5}
/>
)}
</THead>
<TBody>
{canViewOverviewPage && isOverviewLoading && (
@ -1499,9 +1554,9 @@ export const OverviewPage = () => {
style={{ height: "45px" }}
/>
</Td>
{visibleEnvs?.map(({ name, slug }) => (
{visibleEnvs?.map(({ name, slug }, i) => (
<Td
key={`explore-${name}-btn`}
key={`explore-${name}-btn-${i + 1}`}
className="border-0 border-r border-mineshaft-600 p-0"
>
<div

View File

@ -1,8 +1,8 @@
import { subject } from "@casl/ability";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import {
faAngleDown,
faCheck,
faCircle,
faCodeBranch,
faEye,
faEyeSlash,
@ -148,7 +148,7 @@ export const SecretOverviewTableRow = ({
"border-r border-mineshaft-600 px-0 py-3 group-hover:bg-mineshaft-700",
isFormExpanded && "border-t-2 border-mineshaft-500",
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-mineshaft-400" : "",
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
)}
>
@ -174,7 +174,7 @@ export const SecretOverviewTableRow = ({
)}
{isSecretEmpty && (
<Tooltip content="Empty value">
<FontAwesomeIcon icon={faCircle} />
<FontAwesomeIcon size="sm" icon={faCircle} />
</Tooltip>
)}
</div>

View File

@ -166,23 +166,42 @@ export const SelectionPanel = ({
secrets: secretsToDelete
});
}
return {
environment: env.slug
};
});
const results = await Promise.allSettled(promises);
const areEntriesDeleted = results.some((result) => result.status === "fulfilled");
const areAllEntriesDeleted = results.every((result) => result.status === "fulfilled");
const areSomeEntriesDeleted = results.some((result) => result.status === "fulfilled");
const failedEnvs = userAvailableEnvs
.filter(
(env) =>
!results.some(
(result) => result.status === "fulfilled" && result.value.environment === env.slug
)
)
.map((env) => env.name);
if (processedEntries === 0) {
handlePopUpClose("bulkDeleteEntries");
createNotification({
type: "info",
text: "You don't have access to delete selected items"
});
} else if (areEntriesDeleted) {
} else if (areAllEntriesDeleted) {
handlePopUpClose("bulkDeleteEntries");
resetSelectedEntries();
createNotification({
type: "success",
text: "Successfully deleted selected secrets and folders"
});
} else if (areSomeEntriesDeleted) {
createNotification({
type: "warning",
text: `Deletion partially completed. The following environments could not be processed due to conflicts: ${failedEnvs.join(", ")}.`
});
} else {
createNotification({
type: "error",

View File

@ -3,3 +3,33 @@ export const getExpandedRowStyle = (scrollOffset: number) => ({
width: "calc(100vw - 355px)", // 350px accounts for sidebar and margin
maxWidth: "1270px" // largest width of table on ultra-wide
});
type GetHeaderStyleParams = {
collapseEnvironments: boolean;
isLast: boolean;
headerHeight: number;
};
export const getHeaderStyle = ({
collapseEnvironments,
isLast,
headerHeight
}: GetHeaderStyleParams) => {
if (!collapseEnvironments) return undefined;
// scott: this is mostly trial/error to keep centered with skew
if (isLast) {
return {
width: headerHeight * 0.42,
bottom: headerHeight * 0.222,
left: 2
};
}
return {
// scott: this is mostly trial/error to keep centered with skew
width: headerHeight * 0.9,
bottom: headerHeight * 0.45,
left: 24 - (headerHeight * 0.9) / 2.985
};
};

View File

@ -253,7 +253,7 @@ export const SecretDropzone = ({
>
{isLoading ? (
<div className="mb-16 flex items-center justify-center pt-16">
<Lottie icon="infisical_loading" className="h-32 w-32" />
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
</div>
) : (
<div className="flex flex-col items-center justify-center space-y-2">

View File

@ -1,14 +1,14 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createFileRoute, redirect } from "@tanstack/react-router";
// this is done as part of migration for multi product inside project
export const Route = createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval',
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval"
)({
beforeLoad: ({ params, search }) => {
throw redirect({
to: '/projects/$projectId/secret-manager/approval',
to: "/projects/$projectId/secret-manager/approval",
params,
search,
})
},
})
search
});
}
});

View File

@ -43,6 +43,7 @@ import { Route as authProviderSuccessPageRouteImport } from './pages/auth/Provid
import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route'
import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route'
import { Route as adminIntegrationsPageRouteImport } from './pages/admin/IntegrationsPage/route'
import { Route as adminEnvironmentPageRouteImport } from './pages/admin/EnvironmentPage/route'
import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route'
import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route'
import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route'
@ -565,6 +566,12 @@ const adminIntegrationsPageRouteRoute = adminIntegrationsPageRouteImport.update(
} as any,
)
const adminEnvironmentPageRouteRoute = adminEnvironmentPageRouteImport.update({
id: '/environment',
path: '/environment',
getParentRoute: () => adminLayoutRoute,
} as any)
const adminEncryptionPageRouteRoute = adminEncryptionPageRouteImport.update({
id: '/encryption',
path: '/encryption',
@ -2180,6 +2187,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof adminEncryptionPageRouteImport
parentRoute: typeof adminLayoutImport
}
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': {
id: '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
path: '/environment'
fullPath: '/admin/environment'
preLoaderRoute: typeof adminEnvironmentPageRouteImport
parentRoute: typeof adminLayoutImport
}
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': {
id: '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
path: '/integrations'
@ -4120,6 +4134,7 @@ interface adminLayoutRouteChildren {
adminAuthenticationPageRouteRoute: typeof adminAuthenticationPageRouteRoute
adminCachingPageRouteRoute: typeof adminCachingPageRouteRoute
adminEncryptionPageRouteRoute: typeof adminEncryptionPageRouteRoute
adminEnvironmentPageRouteRoute: typeof adminEnvironmentPageRouteRoute
adminIntegrationsPageRouteRoute: typeof adminIntegrationsPageRouteRoute
adminMachineIdentitiesResourcesPageRouteRoute: typeof adminMachineIdentitiesResourcesPageRouteRoute
adminOrganizationResourcesPageRouteRoute: typeof adminOrganizationResourcesPageRouteRoute
@ -4131,6 +4146,7 @@ const adminLayoutRouteChildren: adminLayoutRouteChildren = {
adminAuthenticationPageRouteRoute: adminAuthenticationPageRouteRoute,
adminCachingPageRouteRoute: adminCachingPageRouteRoute,
adminEncryptionPageRouteRoute: adminEncryptionPageRouteRoute,
adminEnvironmentPageRouteRoute: adminEnvironmentPageRouteRoute,
adminIntegrationsPageRouteRoute: adminIntegrationsPageRouteRoute,
adminMachineIdentitiesResourcesPageRouteRoute:
adminMachineIdentitiesResourcesPageRouteRoute,
@ -4333,6 +4349,7 @@ export interface FileRoutesByFullPath {
'/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute
'/admin/environment': typeof adminEnvironmentPageRouteRoute
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
@ -4529,6 +4546,7 @@ export interface FileRoutesByTo {
'/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute
'/admin/environment': typeof adminEnvironmentPageRouteRoute
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/projects/$projectId': typeof projectLayoutGeneralRouteWithChildren
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
@ -4725,6 +4743,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
@ -4934,6 +4953,7 @@ export interface FileRouteTypes {
| '/admin/authentication'
| '/admin/caching'
| '/admin/encryption'
| '/admin/environment'
| '/admin/integrations'
| '/organization/app-connections'
| '/organization/gateways'
@ -5129,6 +5149,7 @@ export interface FileRouteTypes {
| '/admin/authentication'
| '/admin/caching'
| '/admin/encryption'
| '/admin/environment'
| '/admin/integrations'
| '/projects/$projectId'
| '/secret-manager/$projectId'
@ -5323,6 +5344,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/admin/_admin-layout/authentication'
| '/_authenticate/_inject-org-details/admin/_admin-layout/caching'
| '/_authenticate/_inject-org-details/admin/_admin-layout/encryption'
| '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
| '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
@ -5729,6 +5751,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/admin/_admin-layout/authentication",
"/_authenticate/_inject-org-details/admin/_admin-layout/caching",
"/_authenticate/_inject-org-details/admin/_admin-layout/encryption",
"/_authenticate/_inject-org-details/admin/_admin-layout/environment",
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations",
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities",
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations",
@ -5775,6 +5798,10 @@ export const routeTree = rootRoute
"filePath": "admin/EncryptionPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
},
"/_authenticate/_inject-org-details/admin/_admin-layout/environment": {
"filePath": "admin/EnvironmentPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
},
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations": {
"filePath": "admin/IntegrationsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"

View File

@ -8,6 +8,7 @@ const adminRoute = route("/admin", [
index("admin/GeneralPage/route.tsx"),
route("/encryption", "admin/EncryptionPage/route.tsx"),
route("/authentication", "admin/AuthenticationPage/route.tsx"),
route("/environment", "admin/EnvironmentPage/route.tsx"),
route("/integrations", "admin/IntegrationsPage/route.tsx"),
route("/caching", "admin/CachingPage/route.tsx"),
route("/resources/organizations", "admin/OrganizationResourcesPage/route.tsx"),

View File

@ -1,3 +1,11 @@
## 1.6.1 (July 3, 2025)
Changes:
* Added support for `topologySpreadConstraints` configuration in Helm chart for the Infisical deployment
Features:
* `topologySpreadConstraints`: Configure pod distribution across availability zones and nodes for high availability
## 1.5.0 (March 26, 2025)
Changes:

Some files were not shown because too many files have changed in this diff Show More