Compare commits

..

37 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
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
=
7ab67db84d feat: fixed black color in tooltip 2025-07-03 01:18:52 +05:30
=
3a17281e37 feat: resolved tooltip overflow 2025-07-03 00:41:47 +05:30
=
abfe185a5b feat: added autoplay to loading lottie and fixed tooltip in project select 2025-07-02 22:13:37 +05:30
f6c10683a5 misc: add sync for passport middleware 2025-07-02 20:48:24 +08: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
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
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
64 changed files with 1451 additions and 386 deletions

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");
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

@ -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

@ -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,14 +263,21 @@ 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({
200: z.object({
id: z.string(),
organization: z.string(),
orgId: z.string(),
@ -280,7 +288,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
cert: z.string(),
lastUsed: z.date().nullable().optional()
})
.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

@ -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,8 +61,7 @@ export type TSamlLoginDTO = {
export type TSamlConfigServiceFactory = {
createSamlCfg: (arg: TCreateSamlCfgDTO) => Promise<TSamlConfigs>;
updateSamlCfg: (arg: TUpdateSamlCfgDTO) => Promise<TSamlConfigs>;
getSaml: (arg: TGetSamlCfgDTO) => Promise<
| {
getSaml: (arg: TGetSamlCfgDTO) => Promise<{
id: string;
organization: string;
orgId: string;
@ -72,9 +71,7 @@ export type TSamlConfigServiceFactory = {
issuer: string;
cert: string;
lastUsed: Date | null | undefined;
}
| undefined
>;
}>;
samlLogin: (arg: TSamlLoginDTO) => Promise<{
isUserCompleted: boolean;
providerAuthToken: string;

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";
@ -1910,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();
@ -2046,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

@ -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,6 +93,7 @@ export const identityProjectServiceFactory = ({
projectId
);
if (requestedRoleChange !== ProjectMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
ProjectPermissionIdentityActions.GrantPrivileges,
@ -111,6 +112,7 @@ export const identityProjectServiceFactory = ({
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
}
// validate custom roles input
const customInputRoles = roles.filter(

View File

@ -69,6 +69,7 @@ export const identityServiceFactory = ({
orgId
);
const isCustomRole = Boolean(customRole);
if (role !== OrgMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
@ -86,6 +87,7 @@ export const identityServiceFactory = ({
),
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

@ -122,8 +122,8 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
.orWhere((qb) => {
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
void qb
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneMonthAgo)
.where(`${TableName.OrgMembership}.createdAt`, ">", oneWeekAgo);
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo)
.where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo);
});
return memberships;
@ -135,9 +135,22 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
}
};
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,
findRecentInvitedMemberships
findRecentInvitedMemberships,
updateLastInvitedAtByIds
};
};

View File

@ -109,7 +109,12 @@ type TOrgServiceFactoryDep = {
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
orgMembershipDAL: Pick<
TOrgMembershipDALFactory,
"findOrgMembershipById" | "findOne" | "findById" | "findRecentInvitedMemberships" | "updateById"
| "findOrgMembershipById"
| "findOne"
| "findById"
| "findRecentInvitedMemberships"
| "updateById"
| "updateLastInvitedAtByIds"
>;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
@ -763,6 +768,10 @@ export const orgServiceFactory = ({
}
});
await orgMembershipDAL.updateById(inviteeOrgMembership.id, {
lastInvitedAt: new Date()
});
return { signupToken: undefined };
};
@ -1433,6 +1442,7 @@ export const orgServiceFactory = ({
const appCfg = getConfig();
const orgCache: Record<string, { name: string; id: string } | undefined> = {};
const notifiedUsers: string[] = [];
await Promise.all(
invitedUsers.map(async (invitedUser) => {
@ -1463,13 +1473,12 @@ export const orgServiceFactory = ({
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
notifiedUsers.push(invitedUser.id);
}
await orgMembershipDAL.updateById(invitedUser.id, {
lastInvitedAt: new Date()
});
})
);
await orgMembershipDAL.updateLastInvitedAtByIds(notifiedUsers);
};
return {

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

@ -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

@ -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,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

@ -43,6 +43,7 @@ export const Tooltip = ({
onOpenChange={onOpenChange}
>
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
side={position}
align="center"
@ -60,6 +61,7 @@ export const Tooltip = ({
{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

@ -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

@ -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
</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">
</h1>
<p className="w-max justify-center text-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,13 +72,18 @@ 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">
<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>
</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}>
<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.
</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">
<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.
<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

@ -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

@ -891,7 +891,7 @@ export const OverviewPage = () => {
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>
);
}

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

@ -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"),