Compare commits

...

40 Commits

Author SHA1 Message Date
38c9242e5b misc: add plain support for user get token in CLI 2025-07-02 04:45:53 +08:00
cce2a54265 Merge pull request #3883 from Infisical/doc/add-mention-of-default-audience-support
doc: add mention of default audience support for CSI
2025-07-01 14:35:15 -04:00
d1033cb324 Merge pull request #3875 from Infisical/ENG-3009
feat(super-admin): Environment Overrides
2025-07-02 02:18:40 +08:00
7134e1dc66 misc: updated success notif 2025-07-02 02:18:04 +08:00
8aa26b77ed Fix check 2025-07-01 13:11:15 -04:00
4b06880320 Feedback fixes 2025-07-01 11:52:01 -04:00
124cd9f812 Merge pull request #3893 from Infisical/misc/added-missing-project-cert-endpoints-to-open-api-spec
misc: added missing project cert endpoints to open api spec
2025-07-01 23:39:37 +08:00
d531d069d1 Add azure app connection 2025-07-01 11:23:44 -04:00
522a5d477d Merge pull request #3889 from Infisical/minor-access-approval-modal-improvements
improvement(approval-policy): minor create policy layout adjustments
2025-07-01 08:21:26 -07:00
d2f0db669a Merge pull request #3894 from Infisical/fix/address-instance-of-github-dynamic-secret
fix: address instanceof check in github dynamic secret
2025-07-01 23:11:01 +08:00
4dd78d745b fix: address instanceof check in github dynamic secret 2025-07-01 20:45:00 +08:00
4fef5c305d misc: added missing project cert endpoints to open api spec 2025-07-01 18:53:13 +08:00
30f3543850 Merge pull request #3876 from Infisical/ENG-2977
feat(secret-sync): Allow custom field label on 1pass sync
2025-06-30 23:36:22 -04:00
114915f913 Merge pull request #3891 from Infisical/change-request-page-improvements
improvement(secret-approval-request): Color/layout styling adjustments to change request page
2025-06-30 19:35:40 -07:00
b5801af9a8 improvements: address feedback 2025-06-30 18:32:36 -07:00
20366a8c07 improvement: address feedback 2025-06-30 18:09:50 -07:00
447e28511c improvement: update stale/conflict text 2025-06-30 16:44:29 -07:00
650ed656e3 improvement: color/layout styling adjustments to change request page 2025-06-30 16:30:37 -07:00
54ac450b63 improvement: minor layout adjustments 2025-06-30 14:38:23 -07:00
3871fa552c Merge pull request #3888 from Infisical/revert-3885-misc/add-indices-for-referencing-columns-in-identity-access-token
Revert "misc: add indices for referencing columns in identity access token"
2025-06-30 17:27:31 -04:00
9c72ee7f10 Revert "misc: add indices for referencing columns in identity access token" 2025-07-01 05:23:51 +08:00
22e8617661 Merge pull request #3885 from Infisical/misc/add-indices-for-referencing-columns-in-identity-access-token
misc: add indices for referencing columns in identity access token
2025-06-30 17:01:20 -04:00
2f29a513cc misc: make index creation concurrently 2025-07-01 03:36:55 +08:00
cb6c28ac26 UI updates 2025-06-30 14:08:27 -04:00
d3833c33b3 Merge pull request #3878 from Infisical/fix-approval-policy-bypassing
Fix bypassing approval policies
2025-06-30 13:37:28 -04:00
978a3e5828 misc: add indices for referencing columns in identity access token 2025-07-01 01:25:11 +08:00
27bf91e58f Merge pull request #3873 from Infisical/org-access-control-improvements
improvement(org-access-control): Standardize and improve org access control UI
2025-06-30 09:54:42 -07:00
f2c3c76c60 improvement: address feedback on remove rule policy edit 2025-06-30 09:21:00 -07:00
85023916e4 improvement: address feedback 2025-06-30 09:12:47 -07:00
3723afe595 Merge branch 'main' into ENG-3009 2025-06-30 12:01:14 -04:00
02afd6a8e7 Merge pull request #3882 from Infisical/feat/fix-access-token-ips
feat: resolved inefficient join for ip restriction in access token
2025-06-30 21:22:28 +05:30
=
929eac4350 feat: resolved inefficient join for ip restriction in access token 2025-06-30 20:13:26 +05:30
58b61a861a Fix bypassing approval policies 2025-06-28 04:17:09 -04:00
d79a6b8f25 Lint fixes 2025-06-28 03:35:52 -04:00
217a09c97b Docs 2025-06-28 03:14:45 -04:00
a389ede03d Review fixes 2025-06-28 03:01:34 -04:00
10939fecc0 feat(super-admin): Environment Overrides 2025-06-28 02:35:38 -04:00
48f40ff938 improvement: address feedback 2025-06-27 21:00:48 -07:00
ed7d709a70 improvement: standardize and improve org access control 2025-06-27 15:15:12 -07:00
9af5a66bab feat(secret-sync): Allow custom field label on 1pass sync 2025-06-26 16:07:08 -04:00
59 changed files with 1767 additions and 737 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

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

@ -350,6 +350,12 @@ export const accessApprovalRequestServiceFactory = ({
const canBypass = !policy.bypassers.length || policy.bypassers.some((bypasser) => bypasser.userId === actorId);
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypass);
// Calculate break glass attempt before sequence checks
const isBreakGlassApprovalAttempt =
policy.enforcementLevel === EnforcementLevel.Soft &&
actorId === accessApprovalRequest.requestedByUserId &&
status === ApprovalStatus.APPROVED;
const isApprover = policy.approvers.find((approver) => approver.userId === actorId);
// If user is (not an approver OR cant self approve) AND can't bypass policy
if ((!isApprover || (!policy.allowedSelfApprovals && isSelfApproval)) && cannotBypassUnderSoftEnforcement) {
@ -409,15 +415,14 @@ export const accessApprovalRequestServiceFactory = ({
const isApproverOfTheSequence = policy.approvers.find(
(el) => el.sequence === presentSequence.step && el.userId === actorId
);
if (!isApproverOfTheSequence) throw new BadRequestError({ message: "You are not reviewer in this step" });
// Only throw if actor is not the approver and not bypassing
if (!isApproverOfTheSequence && !isBreakGlassApprovalAttempt) {
throw new BadRequestError({ message: "You are not a reviewer in this step" });
}
}
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
const isBreakGlassApprovalAttempt =
policy.enforcementLevel === EnforcementLevel.Soft &&
actorId === accessApprovalRequest.requestedByUserId &&
status === ApprovalStatus.APPROVED;
let reviewForThisActorProcessing: {
id: string;
requestId: string;

View File

@ -1,5 +1,5 @@
import axios from "axios";
import * as jwt from "jsonwebtoken";
import jwt from "jsonwebtoken";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";

View File

@ -2427,7 +2427,8 @@ export const SecretSyncs = {
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault."
},
ONEPASS: {
vaultId: "The ID of the 1Password vault to sync secrets to."
vaultId: "The ID of the 1Password vault to sync secrets to.",
valueLabel: "The label of the entry that holds the secret value."
},
HEROKU: {
app: "The ID of the Heroku app to sync secrets to.",

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

@ -1419,7 +1419,8 @@ export const registerRoutes = async (
const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL,
identityOrgMembershipDAL,
accessTokenQueue
accessTokenQueue,
identityDAL
});
const identityProjectService = identityProjectServiceFactory({
@ -2044,6 +2045,10 @@ export const registerRoutes = async (
cronJobs.push(adminIntegrationsSyncJob);
}
}
const configSyncJob = await superAdminService.initializeEnvConfigSync();
if (configSyncJob) {
cronJobs.push(configSyncJob);
}
server.decorate<FastifyZodProvider["store"]>("store", {
user: userDAL,

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

@ -457,6 +457,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiAlerting],
params: z.object({
projectId: z.string().trim()
}),
@ -487,6 +489,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateCollections],
params: z.object({
projectId: z.string().trim()
}),
@ -549,6 +553,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
projectId: z.string().trim()
}),

View File

@ -17,71 +17,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken)
.where(filter)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.leftJoin(
TableName.IdentityUaClientSecret,
`${TableName.IdentityAccessToken}.identityUAClientSecretId`,
`${TableName.IdentityUaClientSecret}.id`
)
.leftJoin(
TableName.IdentityUniversalAuth,
`${TableName.IdentityUaClientSecret}.identityUAId`,
`${TableName.IdentityUniversalAuth}.id`
)
.leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
.leftJoin(
TableName.IdentityAliCloudAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityAliCloudAuth}.identityId`
)
.leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
.leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
.leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
.leftJoin(
TableName.IdentityKubernetesAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`)
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"),
db
.ref("accessTokenTrustedIps")
.withSchema(TableName.IdentityAliCloudAuth)
.as("accessTokenTrustedIpsAliCloud"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOciAuth).as("accessTokenTrustedIpsOci"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"),
db.ref("name").withSchema(TableName.Identity)
)
.select(db.ref("name").withSchema(TableName.Identity))
.first();
if (!doc) return;
return {
...doc,
trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa,
trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp,
trustedIpsAliCloudAuth: doc.accessTokenTrustedIpsAliCloud,
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
trustedIpsOciAuth: doc.accessTokenTrustedIpsOci,
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap,
trustedIpsAccessTlsCertAuth: []
};
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
}

View File

@ -7,12 +7,14 @@ import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types";
type TIdentityAccessTokenServiceFactoryDep = {
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityDAL: Pick<TIdentityDALFactory, "getTrustedIpsByAuthMethod">;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
accessTokenQueue: Pick<
TAccessTokenQueueServiceFactory,
@ -25,7 +27,8 @@ export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAcces
export const identityAccessTokenServiceFactory = ({
identityAccessTokenDAL,
identityOrgMembershipDAL,
accessTokenQueue
accessTokenQueue,
identityDAL
}: TIdentityAccessTokenServiceFactoryDep) => {
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
const {
@ -190,24 +193,11 @@ export const identityAccessTokenServiceFactory = ({
message: "Failed to authorize revoked access token, access token is revoked"
});
const trustedIpsMap: Record<IdentityAuthMethod, unknown> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
[IdentityAuthMethod.ALICLOUD_AUTH]: identityAccessToken.trustedIpsAliCloudAuth,
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth,
[IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth,
[IdentityAuthMethod.TLS_CERT_AUTH]: identityAccessToken.trustedIpsAccessTlsCertAuth
};
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
if (ipAddress) {
const trustedIps = await identityDAL.getTrustedIpsByAuthMethod(
identityAccessToken.identityId,
identityAccessToken.authMethod as IdentityAuthMethod
);
if (ipAddress && trustedIps) {
checkIPAgainstBlocklist({
ipAddress,
trustedIps: trustedIps as TIp[]

View File

@ -1,5 +1,5 @@
import { TDbClient } from "@app/db";
import { TableName, TIdentities } from "@app/db/schemas";
import { IdentityAuthMethod, TableName, TIdentities } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
@ -8,6 +8,28 @@ export type TIdentityDALFactory = ReturnType<typeof identityDALFactory>;
export const identityDALFactory = (db: TDbClient) => {
const identityOrm = ormify(db, TableName.Identity);
const getTrustedIpsByAuthMethod = async (identityId: string, authMethod: IdentityAuthMethod) => {
const authMethodToTableName = {
[IdentityAuthMethod.TOKEN_AUTH]: TableName.IdentityTokenAuth,
[IdentityAuthMethod.UNIVERSAL_AUTH]: TableName.IdentityUniversalAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: TableName.IdentityKubernetesAuth,
[IdentityAuthMethod.GCP_AUTH]: TableName.IdentityGcpAuth,
[IdentityAuthMethod.ALICLOUD_AUTH]: TableName.IdentityAliCloudAuth,
[IdentityAuthMethod.AWS_AUTH]: TableName.IdentityAwsAuth,
[IdentityAuthMethod.AZURE_AUTH]: TableName.IdentityAzureAuth,
[IdentityAuthMethod.TLS_CERT_AUTH]: TableName.IdentityTlsCertAuth,
[IdentityAuthMethod.OCI_AUTH]: TableName.IdentityOciAuth,
[IdentityAuthMethod.OIDC_AUTH]: TableName.IdentityOidcAuth,
[IdentityAuthMethod.JWT_AUTH]: TableName.IdentityJwtAuth,
[IdentityAuthMethod.LDAP_AUTH]: TableName.IdentityLdapAuth
} as const;
const tableName = authMethodToTableName[authMethod];
if (!tableName) return;
const data = await db(tableName).where({ identityId }).first();
if (!data) return;
return data.accessTokenTrustedIps;
};
const getIdentitiesByFilter = async ({
limit,
offset,
@ -38,5 +60,5 @@ export const identityDALFactory = (db: TDbClient) => {
}
};
return { ...identityOrm, getIdentitiesByFilter };
return { ...identityOrm, getTrustedIpsByAuthMethod, getIdentitiesByFilter };
};

View File

@ -392,7 +392,12 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityOrgMembership}.identityId`)
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.orderBy(
orderBy === OrgIdentityOrderBy.Role
? `${TableName.IdentityOrgMembership}.${orderBy}`
: `${TableName.Identity}.${orderBy}`,
orderDirection
)
.select(`${TableName.IdentityOrgMembership}.id`)
.select<{ id: string; total_count: string }>(
db.raw(
@ -523,6 +528,23 @@ export const identityOrgDALFactory = (db: TDbClient) => {
if (orderBy === OrgIdentityOrderBy.Name) {
void query.orderBy("identityName", orderDirection);
} else if (orderBy === OrgIdentityOrderBy.Role) {
void query.orderByRaw(
`
CASE
WHEN ??.role = ?
THEN ??.slug
ELSE ??.role
END ?
`,
[
TableName.IdentityOrgMembership,
"custom",
TableName.OrgRoles,
TableName.IdentityOrgMembership,
db.raw(orderDirection)
]
);
}
const docs = await query;

View File

@ -46,8 +46,8 @@ export type TListOrgIdentitiesByOrgIdDTO = {
} & TOrgPermission;
export enum OrgIdentityOrderBy {
Name = "name"
// Role = "role"
Name = "name",
Role = "role"
}
export type TSearchOrgIdentitiesByOrgIdDAL = {

View File

@ -6,7 +6,6 @@ import {
TOnePassListVariablesResponse,
TOnePassSyncWithCredentials,
TOnePassVariable,
TOnePassVariableDetails,
TPostOnePassVariable,
TPutOnePassVariable
} from "@app/services/secret-sync/1password/1password-sync-types";
@ -14,7 +13,10 @@ import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listOnePassItems = async ({ instanceUrl, apiToken, vaultId }: TOnePassListVariables) => {
// This should not be changed or it may break existing logic
const VALUE_LABEL_DEFAULT = "value";
const listOnePassItems = async ({ instanceUrl, apiToken, vaultId, valueLabel }: TOnePassListVariables) => {
const { data } = await request.get<TOnePassListVariablesResponse>(`${instanceUrl}/v1/vaults/${vaultId}/items`, {
headers: {
Authorization: `Bearer ${apiToken}`,
@ -22,36 +24,49 @@ const listOnePassItems = async ({ instanceUrl, apiToken, vaultId }: TOnePassList
}
});
const result: Record<string, TOnePassVariable & { value: string; fieldId: string }> = {};
const items: Record<string, TOnePassVariable & { value: string; fieldId: string }> = {};
const duplicates: Record<string, string> = {};
for await (const s of data) {
const { data: secret } = await request.get<TOnePassVariableDetails>(
`${instanceUrl}/v1/vaults/${vaultId}/items/${s.id}`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
// eslint-disable-next-line no-continue
if (s.category !== "API_CREDENTIAL") continue;
const value = secret.fields.find((f) => f.label === "value")?.value;
const fieldId = secret.fields.find((f) => f.label === "value")?.id;
if (items[s.title]) {
duplicates[s.id] = s.title;
// eslint-disable-next-line no-continue
continue;
}
const { data: secret } = await request.get<TOnePassVariable>(`${instanceUrl}/v1/vaults/${vaultId}/items/${s.id}`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
const valueField = secret.fields.find((f) => f.label === valueLabel);
// eslint-disable-next-line no-continue
if (!value || !fieldId) continue;
if (!valueField || !valueField.value || !valueField.id) continue;
result[s.title] = {
items[s.title] = {
...secret,
value,
fieldId
value: valueField.value,
fieldId: valueField.id
};
}
return result;
return { items, duplicates };
};
const createOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemTitle, itemValue }: TPostOnePassVariable) => {
const createOnePassItem = async ({
instanceUrl,
apiToken,
vaultId,
itemTitle,
itemValue,
valueLabel
}: TPostOnePassVariable) => {
return request.post(
`${instanceUrl}/v1/vaults/${vaultId}/items`,
{
@ -63,7 +78,7 @@ const createOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemTitle, it
tags: ["synced-from-infisical"],
fields: [
{
label: "value",
label: valueLabel,
value: itemValue,
type: "CONCEALED"
}
@ -85,7 +100,9 @@ const updateOnePassItem = async ({
itemId,
fieldId,
itemTitle,
itemValue
itemValue,
valueLabel,
otherFields
}: TPutOnePassVariable) => {
return request.put(
`${instanceUrl}/v1/vaults/${vaultId}/items/${itemId}`,
@ -98,9 +115,10 @@ const updateOnePassItem = async ({
},
tags: ["synced-from-infisical"],
fields: [
...otherFields,
{
id: fieldId,
label: "value",
label: valueLabel,
value: itemValue,
type: "CONCEALED"
}
@ -128,13 +146,18 @@ export const OnePassSyncFns = {
const {
connection,
environment,
destinationConfig: { vaultId }
destinationConfig: { vaultId, valueLabel }
} = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials;
const items = await listOnePassItems({ instanceUrl, apiToken, vaultId });
const { items, duplicates } = await listOnePassItems({
instanceUrl,
apiToken,
vaultId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry;
@ -148,10 +171,19 @@ export const OnePassSyncFns = {
itemTitle: key,
itemValue: value,
itemId: items[key].id,
fieldId: items[key].fieldId
fieldId: items[key].fieldId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT,
otherFields: items[key].fields.filter((field) => field.label !== (valueLabel || VALUE_LABEL_DEFAULT))
});
} else {
await createOnePassItem({ instanceUrl, apiToken, vaultId, itemTitle: key, itemValue: value });
await createOnePassItem({
instanceUrl,
apiToken,
vaultId,
itemTitle: key,
itemValue: value,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
}
} catch (error) {
throw new SecretSyncError({
@ -163,7 +195,28 @@ export const OnePassSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const [key, variable] of Object.entries(items)) {
// Delete duplicate item entries
for await (const [itemId, key] of Object.entries(duplicates)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
try {
await deleteOnePassItem({
instanceUrl,
apiToken,
vaultId,
itemId
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
// Delete item entries that are not in secretMap
for await (const [key, item] of Object.entries(items)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue;
@ -173,7 +226,7 @@ export const OnePassSyncFns = {
instanceUrl,
apiToken,
vaultId,
itemId: variable.id
itemId: item.id
});
} catch (error) {
throw new SecretSyncError({
@ -187,13 +240,18 @@ export const OnePassSyncFns = {
removeSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { vaultId }
destinationConfig: { vaultId, valueLabel }
} = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials;
const items = await listOnePassItems({ instanceUrl, apiToken, vaultId });
const { items } = await listOnePassItems({
instanceUrl,
apiToken,
vaultId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
for await (const [key, item] of Object.entries(items)) {
if (key in secretMap) {
@ -216,12 +274,19 @@ export const OnePassSyncFns = {
getSecrets: async (secretSync: TOnePassSyncWithCredentials) => {
const {
connection,
destinationConfig: { vaultId }
destinationConfig: { vaultId, valueLabel }
} = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials;
return listOnePassItems({ instanceUrl, apiToken, vaultId });
const res = await listOnePassItems({
instanceUrl,
apiToken,
vaultId,
valueLabel: valueLabel || VALUE_LABEL_DEFAULT
});
return Object.fromEntries(Object.entries(res.items).map(([key, item]) => [key, { value: item.value }]));
}
};

View File

@ -11,7 +11,8 @@ import {
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const OnePassSyncDestinationConfigSchema = z.object({
vaultId: z.string().trim().min(1, "Vault required").describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.vaultId)
vaultId: z.string().trim().min(1, "Vault required").describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.vaultId),
valueLabel: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.valueLabel)
});
const OnePassSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };

View File

@ -14,29 +14,32 @@ export type TOnePassSyncWithCredentials = TOnePassSync & {
connection: TOnePassConnection;
};
type Field = {
id: string;
type: string; // CONCEALED, STRING
label: string;
value: string;
};
export type TOnePassVariable = {
id: string;
title: string;
category: string; // API_CREDENTIAL, SECURE_NOTE, LOGIN, etc
};
export type TOnePassVariableDetails = TOnePassVariable & {
fields: {
id: string;
type: string; // CONCEALED, STRING
label: string;
value: string;
}[];
fields: Field[];
};
export type TOnePassListVariablesResponse = TOnePassVariable[];
export type TOnePassListVariables = {
type TOnePassBase = {
apiToken: string;
instanceUrl: string;
vaultId: string;
};
export type TOnePassListVariables = TOnePassBase & {
valueLabel: string;
};
export type TPostOnePassVariable = TOnePassListVariables & {
itemTitle: string;
itemValue: string;
@ -47,8 +50,9 @@ export type TPutOnePassVariable = TOnePassListVariables & {
fieldId: string;
itemTitle: string;
itemValue: string;
otherFields: Field[];
};
export type TDeleteOnePassVariable = TOnePassListVariables & {
export type TDeleteOnePassVariable = TOnePassBase & {
itemId: string;
};

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

@ -114,6 +114,11 @@ var userGetTokenCmd = &cobra.Command{
loggedInUserDetails = util.EstablishUserLoginSession()
}
plain, err := cmd.Flags().GetBool("plain")
if err != nil {
util.HandleError(err, "[infisical user get token]: Unable to get plain flag")
}
if err != nil {
util.HandleError(err, "[infisical user get token]: Unable to get logged in user token")
}
@ -135,8 +140,12 @@ var userGetTokenCmd = &cobra.Command{
util.HandleError(err, "[infisical user get token]: Unable to parse token payload")
}
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken)
if plain {
fmt.Println(loggedInUserDetails.UserCredentials.JTWToken)
} else {
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken)
}
},
}
@ -240,7 +249,10 @@ var domainCmd = &cobra.Command{
func init() {
updateCmd.AddCommand(domainCmd)
userCmd.AddCommand(updateCmd)
userGetTokenCmd.Flags().Bool("plain", false, "print token without formatting")
userGetCmd.AddCommand(userGetTokenCmd)
userCmd.AddCommand(userGetCmd)
userCmd.AddCommand(switchCmd)
rootCmd.AddCommand(userCmd)

View File

@ -35,19 +35,40 @@ infisical user update domain
<Accordion title="infisical user get token">
Use this command to get your current Infisical access token and session information. This command requires you to be logged in.
The command will display:
The command will display:
- Your session ID
- Your full JWT access token
- Your session ID
- Your full JWT access token
```bash
infisical user get token
```
```bash
infisical user get token
```
Example output:
Example output:
```bash
Session ID: abc123-xyz-456
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### Flags
<Accordion title="--plain">
Output only the JWT token without formatting (no session ID)
Default value: `false`
```bash
# Example
infisical user get token --plain
```
Example output:
```bash
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
</Accordion>
```bash
Session ID: abc123-xyz-456
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
</Accordion>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 641 KiB

View File

@ -36,6 +36,7 @@ description: "Learn how to configure a 1Password Sync for Infisical."
- **1Password Connection**: The 1Password Connection to authenticate with.
- **Vault**: The 1Password vault to sync secrets to.
- **Value Label**: The label of the 1Password item field that will hold your secret value.
</Step>
<Step title="Configure sync options">
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
@ -94,7 +95,8 @@ description: "Learn how to configure a 1Password Sync for Infisical."
"initialSyncBehavior": "overwrite-destination"
},
"destinationConfig": {
"vaultId": "..."
"vaultId": "...",
"valueLabel": "value"
}
}'
```
@ -145,7 +147,8 @@ description: "Learn how to configure a 1Password Sync for Infisical."
},
"destination": "1password",
"destinationConfig": {
"vaultId": "..."
"vaultId": "...",
"valueLabel": "value"
}
}
}
@ -160,4 +163,7 @@ description: "Learn how to configure a 1Password Sync for Infisical."
Infisical can only perform CRUD operations on the following item types:
- API Credentials
</Accordion>
<Accordion title="What is a 'Value Label'?">
It's the label of the 1Password item field which will hold your secret value. For example, if you were to sync Infisical secret 'foo: bar', the 1Password item equivalent would have an item title of 'foo', and a field on that item 'value: bar'. The field label 'value' is what gets changed by this option.
</Accordion>
</AccordionGroup>

View File

@ -4,7 +4,7 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2";
import {
TOnePassVault,
useOnePassConnectionListVaults
@ -32,6 +32,7 @@ export const OnePassSyncFields = () => {
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.vaultId", "");
setValue("destinationConfig.valueLabel", "");
}}
/>
@ -69,6 +70,22 @@ export const OnePassSyncFields = () => {
</FormControl>
)}
/>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
isOptional
label="Value Label"
tooltipText="It's the label of the 1Password item field which will hold your secret value. For example, if you were to sync Infisical secret 'foo: bar', the 1Password item equivalent would have an item title of 'foo', and a field on that item 'value: bar'. The field label 'value' is what gets changed by this option."
>
<Input value={value} onChange={onChange} placeholder="value" />
</FormControl>
)}
control={control}
name="destinationConfig.valueLabel"
/>
</>
);
};

View File

@ -6,7 +6,15 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
export const OnePassSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.OnePass }>();
const vaultId = watch("destinationConfig.vaultId");
const [vaultId, valueLabel] = watch([
"destinationConfig.vaultId",
"destinationConfig.valueLabel"
]);
return <GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>;
return (
<>
<GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>
<GenericFieldLabel label="Value Key">{valueLabel || "value"}</GenericFieldLabel>
</>
);
};

View File

@ -7,7 +7,8 @@ export const OnePassSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.OnePass),
destinationConfig: z.object({
vaultId: z.string().trim().min(1, "Vault ID required")
vaultId: z.string().trim().min(1, "Vault ID required"),
valueLabel: z.string().trim().optional()
})
})
);

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

@ -64,7 +64,7 @@ export const Pagination = ({
<FontAwesomeIcon className="text-xs" icon={faCaretDown} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-fit">
<DropdownMenuContent sideOffset={2} className="min-w-fit">
{perPageList.map((perPageOption) => (
<DropdownMenuItem
key={`pagination-per-page-options-${perPageOption}`}

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

@ -81,7 +81,8 @@ export const useSearchIdentities = (dto: TSearchIdentitiesDTO) => {
search
});
return data;
}
},
placeholderData: (previousData) => previousData
});
};

View File

@ -154,6 +154,6 @@ export type TOrgIdentitiesList = {
};
export enum OrgIdentityOrderBy {
Name = "name"
// Role = "role"
Name = "name",
Role = "role"
}

View File

@ -6,6 +6,7 @@ export type TOnePassSync = TRootSecretSync & {
destination: SecretSync.OnePass;
destinationConfig: {
vaultId: string;
valueLabel?: string;
};
connection: {
app: AppConnection.OnePass;

View File

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

View File

@ -164,7 +164,10 @@ export const MinimizedOrgSidebar = () => {
const handleCopyToken = async () => {
try {
await window.navigator.clipboard.writeText(getAuthToken());
createNotification({ type: "success", text: "Copied current login session token to clipboard" });
createNotification({
type: "success",
text: "Copied current login session token to clipboard"
});
} catch (error) {
console.log(error);
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });

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"
description="Manage the environment for your Infisical instance."
/>
<EnvironmentPageForm />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,245 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Control, Controller, useForm, useWatch } from "react-hook-form";
import {
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>
</div>
<p className="text-sm text-bunker-300">Override specific environment variables.</p>
</div>
<div className="flex flex-row gap-2">
<Button
type="submit"
variant="outline_bg"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save Overrides
</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: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Environment",
link: linkOptions({
to: "/admin/environment"
})
}
]
};
}
});

View File

@ -56,12 +56,12 @@ export const OrgGroupsSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Groups</p>
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}

View File

@ -2,14 +2,17 @@ import { useMemo } from "react";
import {
faArrowDown,
faArrowUp,
faEllipsis,
faCopy,
faEdit,
faEllipsisV,
faMagnifyingGlass,
faSearch,
faTrash,
faUserGroup,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
@ -261,7 +264,8 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
<Select
value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed}
className="w-48 bg-mineshaft-600"
className="h-8 w-48 bg-mineshaft-700"
position="popper"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
@ -282,13 +286,19 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
className="w-6"
colorSchema="secondary"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={(e) => {
e.stopPropagation();
createNotification({
@ -306,10 +316,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("group", {
@ -320,7 +327,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
customRole
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Edit Group
</DropdownMenuItem>
@ -332,10 +339,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
icon={<FontAwesomeIcon icon={faUserGroup} />}
onClick={() =>
navigate({
to: "/organization/groups/$groupId",
@ -344,7 +348,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
}
})
}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Manage Members
</DropdownMenuItem>
@ -356,11 +360,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", {
@ -368,7 +368,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
name
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Delete Group
</DropdownMenuItem>

View File

@ -1,4 +1,4 @@
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
@ -71,20 +71,22 @@ export const IdentitySection = withPermission(
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<div className="flex w-full justify-end pr-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/identities/overview"
className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white"
>
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
<div className="ml-1 mt-[0.16rem] 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>
<OrgPermissionCan
@ -93,7 +95,7 @@ export const IdentitySection = withPermission(
>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {

View File

@ -1,12 +1,15 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useCallback, useState } from "react";
import {
faArrowDown,
faArrowUp,
faEllipsis,
faCheckCircle,
faChevronRight,
faEdit,
faEllipsisV,
faFilter,
faMagnifyingGlass,
faServer
faServer,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
@ -15,19 +18,18 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
EmptyState,
FormControl,
IconButton,
Input,
Pagination,
Popover,
PopoverContent,
PopoverTrigger,
Select,
SelectItem,
Spinner,
@ -38,7 +40,6 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
@ -63,6 +64,10 @@ type Props = {
) => void;
};
type Filter = {
roles: string[];
};
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
@ -90,7 +95,9 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage);
};
const [filteredRoles, setFilteredRoles] = useState<string[]>([]);
const [filter, setFilter] = useState<Filter>({
roles: []
});
const organizationId = currentOrg?.id || "";
@ -103,7 +110,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
orderBy,
search: {
name: debouncedSearch ? { $contains: debouncedSearch } : undefined,
role: filteredRoles?.length ? { $in: filteredRoles } : undefined
role: filter.roles?.length ? { $in: filter.roles } : undefined
}
});
@ -113,7 +120,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
offset,
setPage
});
const filterForm = useForm<{ roles: string }>();
const { data: roles } = useGetOrgRoles(organizationId);
@ -153,79 +159,80 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
}
};
const handleRoleToggle = useCallback(
(roleSlug: string) =>
setFilter((state) => {
const currentRoles = state.roles || [];
if (currentRoles.includes(roleSlug)) {
return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) };
}
return { ...state, roles: [...currentRoles, roleSlug] };
}),
[]
);
const isTableFiltered = Boolean(filter.roles.length);
return (
<div>
<div className="mb-4 flex items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Identities"
variant="plain"
size="sm"
className={twMerge(
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-0">
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
<DropdownSubMenu>
<DropdownSubMenuTrigger
iconPos="right"
icon={<FontAwesomeIcon icon={faChevronRight} size="sm" />}
>
Roles
</DropdownSubMenuTrigger>
<DropdownSubMenuContent className="thin-scrollbar max-h-[20rem] overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Roles to Filter Identities
</DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
handleRoleToggle(slug);
}}
key={id}
icon={filter.roles.includes(slug) && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: "#bec2c8" }}
/>
{name}
</div>
</DropdownMenuItem>
))}
</DropdownSubMenuContent>
</DropdownSubMenu>
</DropdownMenuContent>
</DropdownMenu>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search identities by name..."
/>
<div>
<Popover>
<PopoverTrigger>
<IconButton
ariaLabel="filter"
variant="outline_bg"
className={filteredRoles?.length ? "border-primary" : ""}
>
<Tooltip content="Advance Filter">
<FontAwesomeIcon icon={faFilter} />
</Tooltip>
</IconButton>
</PopoverTrigger>
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl">
<div className="mb-4 border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
Advance Filter
</div>
<form
onSubmit={filterForm.handleSubmit((el) => {
setFilteredRoles(el.roles?.split(",")?.filter(Boolean) || []);
})}
>
<Controller
control={filterForm.control}
name="roles"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Roles"
helperText="Eg: admin,viewer"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex items-center space-x-2">
<Button
type="submit"
size="xs"
colorSchema="primary"
variant="outline_bg"
className="mt-4"
>
Apply Filter
</Button>
{Boolean(filteredRoles.length) && (
<Button
size="xs"
variant="link"
className="ml-4 mt-4"
onClick={() => {
filterForm.reset({ roles: "" });
setFilteredRoles([]);
}}
>
Clear
</Button>
)}
</div>
</form>
</PopoverContent>
</Popover>
</div>
</div>
<TableContainer>
<Table>
@ -251,8 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</IconButton>
</div>
</Th>
<Th>Role</Th>
{/* <Th>
<Th>
<div className="flex items-center">
Role
<IconButton
@ -271,7 +277,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
/>
</IconButton>
</div>
</Th> */}
</Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
@ -303,7 +309,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<Select
value={role === "custom" ? (customRole?.slug as string) : role}
isDisabled={!isAllowed}
className="w-48 bg-mineshaft-600"
className="h-8 w-48 bg-mineshaft-700"
position="popper"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
@ -324,21 +331,24 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="flex justify-center hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
className="w-6"
colorSchema="secondary"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="mt-3 p-1">
<DropdownMenuContent sideOffset={2} align="end">
<OrgPermissionCan
I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={(e) => {
e.stopPropagation();
navigate({
@ -348,7 +358,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
}
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
Edit Identity
</DropdownMenuItem>
@ -360,11 +370,6 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteIdentity", {
@ -372,7 +377,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
name
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Identity
</DropdownMenuItem>
@ -398,7 +404,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
{!isPending && data && data?.identities.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0 || filteredRoles?.length > 0
debouncedSearch.trim().length > 0 || filter.roles?.length > 0
? "No identities match search filter"
: "No identities have been created in this organization"
}

View File

@ -115,12 +115,12 @@ export const OrgMembersSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddMemberModal()}

View File

@ -4,11 +4,14 @@ import {
faArrowUp,
faCheckCircle,
faChevronRight,
faEllipsis,
faEdit,
faEllipsisV,
faFilter,
faMagnifyingGlass,
faSearch,
faUsers
faUsers,
faUserSlash,
faUserXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
@ -79,7 +82,8 @@ type Props = {
enum OrgMembersOrderBy {
Name = "firstName",
Email = "email"
Email = "email",
Role = "role"
}
type Filter = {
@ -99,8 +103,10 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
const { data: serverDetails } = useFetchServerStatus();
const { data: members = [], isPending: isMembersLoading } = useGetOrgUsers(orgId);
const { mutateAsync: resendOrgMemberInvitation } = useResendOrgMemberInvitation();
const { mutateAsync: resendOrgMemberInvitation, isPending: isResendInvitePending } =
useResendOrgMemberInvitation();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const [resendInviteId, setResendInviteId] = useState<string | null>(null);
const onRoleChange = async (membershipId: string, role: string) => {
if (!currentOrg?.id) return;
@ -136,6 +142,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
};
const onResendInvite = async (membershipId: string) => {
setResendInviteId(membershipId);
try {
const signupToken = await resendOrgMemberInvitation({
membershipId
@ -156,6 +163,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
text: "Failed to resend org invitation",
type: "error"
});
} finally {
setResendInviteId(null);
}
};
@ -229,6 +238,16 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
valueOne = memberOne.user.email || memberOne.inviteEmail;
valueTwo = memberTwo.user.email || memberTwo.inviteEmail;
break;
case OrgMembersOrderBy.Role:
valueOne =
memberOne.role === "custom"
? findRoleFromId(memberOne.roleId)!.slug
: memberOne.role;
valueTwo =
memberTwo.role === "custom"
? findRoleFromId(memberTwo.roleId)!.slug
: memberTwo.role;
break;
case OrgMembersOrderBy.Name:
default:
valueOne = memberOne.user.firstName;
@ -284,7 +303,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
@ -378,7 +397,26 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
</IconButton>
</div>
</Th>
<Th>Role</Th>
<Th className="w-1/3">
<div className="flex items-center">
Role
<IconButton
variant="plain"
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(OrgMembersOrderBy.Role)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === OrgMembersOrderBy.Role
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
@ -398,7 +436,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
isActive
}) => {
const name =
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : "-";
u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null;
const email = u?.email || inviteEmail;
const username = u?.username ?? inviteEmail ?? "-";
return (
@ -415,7 +453,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
}
>
<Td className={isActive ? "" : "text-mineshaft-400"}>
{name}
{name ?? <span className="text-mineshaft-400">Not Set</span>}
{u.superAdmin && (
<Badge variant="primary" className="ml-2">
Server Admin
@ -429,79 +467,77 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<>
{!isActive && (
<Button
isDisabled
className="w-40"
colorSchema="primary"
variant="outline_bg"
onClick={() => {}}
>
Suspended
</Button>
)}
{isActive && status === "accepted" && (
<Select
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
isDisabled={userId === u?.id || !isAllowed}
className="w-48 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{(roles || [])
.filter(({ slug }) =>
slug === "owner" ? isIamOwner || role === "owner" : true
)
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
</SelectItem>
))}
</Select>
)}
{isActive &&
(status === "invited" || status === "verified") &&
email &&
serverDetails?.emailConfigured && (
<Select
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
isDisabled={userId === u?.id || !isAllowed}
className="h-8 w-48 bg-mineshaft-700"
position="popper"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{(roles || [])
.filter(({ slug }) =>
slug === "owner" ? isIamOwner || role === "owner" : true
)
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
</SelectItem>
))}
</Select>
)}
</OrgPermissionCan>
</Td>
<Td>
<div className="flex items-center justify-end gap-6">
{isActive &&
(status === "invited" || status === "verified") &&
email &&
serverDetails?.emailConfigured && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
className="w-48"
isDisabled={!isAllowed || isResendInvitePending}
className="h-8 border-mineshaft-600 bg-mineshaft-700 font-normal"
colorSchema="primary"
variant="outline_bg"
isLoading={
isResendInvitePending && resendInviteId === orgMembershipId
}
onClick={(e) => {
onResendInvite(orgMembershipId);
e.stopPropagation();
}}
>
Resend invite
Resend Invite
</Button>
)}
</>
)}
</OrgPermissionCan>
</Td>
<Td>
{userId !== u?.id && (
</OrgPermissionCan>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className={twMerge("w-6", userId === u?.id && "opacity-50")}
variant="plain"
isDisabled={userId === u?.id}
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent sideOffset={2} align="end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
navigate({
@ -511,7 +547,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
}
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit User
</DropdownMenuItem>
@ -523,15 +560,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
>
{(isAllowed) => (
<DropdownMenuItem
className={
isActive
? twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)
: ""
}
icon={<FontAwesomeIcon icon={faUserSlash} />}
onClick={async (e) => {
e.stopPropagation();
@ -560,7 +589,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
username
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
>
{`${isActive ? "Deactivate" : "Activate"} User`}
</DropdownMenuItem>
@ -572,11 +601,6 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
@ -593,7 +617,8 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
username
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faUserXmark} />}
>
Remove User
</DropdownMenuItem>
@ -601,7 +626,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</Td>
</Tr>
);

View File

@ -6,10 +6,10 @@ export const OrgRoleTabSection = () => {
return (
<motion.div
key="role-list"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: -30 }}
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
exit={{ opacity: 0, translateX: 30 }}
>
<OrgRoleTable />
</motion.div>

View File

@ -1,4 +1,17 @@
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
import { useMemo } from "react";
import {
faArrowDown,
faArrowUp,
faCopy,
faEdit,
faEllipsisV,
faEye,
faIdBadge,
faMagnifyingGlass,
faPlus,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@ -14,6 +27,10 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
@ -30,13 +47,25 @@ import {
useOrganization,
useSubscription
} from "@app/context";
import { isCustomOrgRole } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks";
import { isCustomOrgRole, isCustomProjectRole } from "@app/helpers/roles";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TOrgRole } from "@app/hooks/api/roles/types";
import { DuplicateOrgRoleModal } from "@app/pages/organization/RoleByIDPage/components/DuplicateOrgRoleModal";
import { RoleModal } from "@app/pages/organization/RoleByIDPage/components/RoleModal";
enum RolesOrderBy {
Name = "name",
Slug = "slug",
Type = "type"
}
export const OrgRoleTable = () => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
@ -93,14 +122,89 @@ export const OrgRoleTable = () => {
}
};
const {
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy,
search,
setSearch,
page,
perPage,
setPerPage,
setPage,
offset
} = usePagination<RolesOrderBy>(RolesOrderBy.Type, {
initPerPage: getUserTablePreference("orgRolesTable", PreferenceKey.PerPage, 20)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("orgRolesTable", PreferenceKey.PerPage, newPerPage);
};
const filteredRoles = useMemo(
() =>
roles
?.filter((role) => {
const { slug, name } = role;
const searchValue = search.trim().toLowerCase();
return (
name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue)
);
})
.sort((a, b) => {
const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case RolesOrderBy.Slug:
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
case RolesOrderBy.Type: {
const roleOneValue = isCustomOrgRole(roleOne.slug) ? -1 : 1;
const roleTwoValue = isCustomOrgRole(roleTwo.slug) ? -1 : 1;
return roleTwoValue - roleOneValue;
}
case RolesOrderBy.Name:
default:
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
}
}) ?? [],
[roles, orderDirection, search, orderBy]
);
useResetPageHelper({
totalCount: filteredRoles.length,
offset,
setPage
});
const handleSort = (column: RolesOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: RolesOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Organization Roles</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
@ -113,18 +217,63 @@ export const OrgRoleTable = () => {
)}
</OrgPermissionCan>
</div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search roles..."
className="flex-1"
containerClassName="mb-4"
/>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Slug
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Slug)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Slug)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Type
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Type)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Type)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Type)} />
</IconButton>
</div>
</Th>
<Th aria-label="actions" className="w-5" />
</Tr>
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={3} innerKey="org-roles" />}
{roles?.map((role) => {
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{filteredRoles?.slice(offset, perPage * page).map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
const isDefaultOrgRole = isCustomOrgRole(slug)
@ -162,23 +311,30 @@ export const OrgRoleTable = () => {
<Td className="max-w-md overflow-hidden text-ellipsis whitespace-nowrap">
{slug}
</Td>
<Td>
<Badge className="w-min whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
{isCustomProjectRole(slug) ? "Custom" : "Default"}
</Badge>
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
navigate({
@ -188,7 +344,8 @@ export const OrgRoleTable = () => {
}
});
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
>
{`${isNonMutatable ? "View" : "Edit"} Role`}
</DropdownMenuItem>
@ -200,14 +357,12 @@ export const OrgRoleTable = () => {
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("duplicateRole", role);
}}
disabled={!isAllowed}
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faCopy} />}
>
Duplicate Role
</DropdownMenuItem>
@ -220,14 +375,12 @@ export const OrgRoleTable = () => {
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
disabled={!isAllowed}
isDisabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
handleSetRoleAsDefault(slug);
}}
icon={<FontAwesomeIcon icon={faIdBadge} />}
>
Set as Default Role
</DropdownMenuItem>
@ -250,16 +403,12 @@ export const OrgRoleTable = () => {
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed && !isDefaultOrgRole
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
}}
disabled={!isAllowed || isDefaultOrgRole}
icon={<FontAwesomeIcon icon={faTrash} />}
isDisabled={!isAllowed || isDefaultOrgRole}
>
Delete Role
</DropdownMenuItem>
@ -276,6 +425,25 @@ export const OrgRoleTable = () => {
})}
</TBody>
</Table>
{Boolean(filteredRoles?.length) && (
<Pagination
count={filteredRoles!.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!filteredRoles?.length && !isRolesLoading && (
<EmptyState
title={
roles?.length
? "No roles match search..."
: "This organization does not have any roles"
}
icon={roles?.length ? faSearch : undefined}
/>
)}
</TableContainer>
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal

View File

@ -197,7 +197,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
"flex h-[2.375rem] w-[2.6rem] items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
@ -298,7 +298,8 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
{!isMembersLoading &&
filteredUsers.slice(offset, perPage * page).map((projectMember) => {
const { user: u, inviteEmail, id: membershipId, roles } = projectMember;
const name = u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : "-";
const name =
u.firstName || u.lastName ? `${u.firstName} ${u.lastName || ""}` : null;
const email = u?.email || inviteEmail;
return (
@ -328,7 +329,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
})
}
>
<Td>{name}</Td>
<Td>{name ?? <span className="text-mineshaft-400">Not Set</span>}</Td>
<Td>{email}</Td>
<Td>
<div className="flex items-center space-x-2">

View File

@ -236,7 +236,7 @@ export const ProjectRoleList = () => {
</Tr>
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />}
{isRolesLoading && <TableSkeleton columns={4} innerKey="project-roles" />}
{filteredRoles?.slice(offset, perPage * page).map((role) => {
const { id, name, slug } = role;
const isNonMutatable = Object.values(ProjectMembershipRole).includes(

View File

@ -12,7 +12,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2";
import { Button, Checkbox, IconButton, Select, SelectItem, Tag, Tooltip } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
@ -241,16 +241,19 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
/>
</Tooltip>
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="ml-auto mr-3"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
Remove Rule
</Button>
<Tooltip content="Remove Rule">
<IconButton
ariaLabel="Remove rule"
colorSchema="danger"
variant="plain"
size="xs"
className="ml-auto mr-3 rounded"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
)}
{!isDisabled && (
<Tooltip position="left" content="Drag to reorder permission">
@ -271,16 +274,19 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
<div className="flex w-full justify-between">
<div className="mb-2">Actions</div>
{!isDisabled && !isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="ml-auto"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
Remove Rule
</Button>
<Tooltip content="Remove Rule">
<IconButton
ariaLabel="Remove rule"
colorSchema="danger"
variant="plain"
size="xs"
className="ml-auto rounded"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
)}
</div>
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">

View File

@ -439,39 +439,35 @@ export const ReviewAccessRequestModal = ({
</div>
) : (
<>
{isSoftEnforcement &&
request.isRequestedByCurrentUser &&
!(request.isApprover && request.isSelfApproveAllowed) &&
canBypass && (
<div className="mt-2 flex flex-col space-y-2">
<Checkbox
onCheckedChange={(checked) => setBypassApproval(checked === true)}
isChecked={bypassApproval}
id="byPassApproval"
className={twMerge("mr-2", bypassApproval ? "border-red/30 bg-red/10" : "")}
{isSoftEnforcement && request.isRequestedByCurrentUser && canBypass && (
<div className="mt-2 flex flex-col space-y-2">
<Checkbox
onCheckedChange={(checked) => setBypassApproval(checked === true)}
isChecked={bypassApproval}
id="byPassApproval"
className={twMerge("mr-2", bypassApproval ? "!border-red/30 !bg-red/10" : "")}
>
<span className="text-xs text-red">
Approve without waiting for requirements to be met (bypass policy protection)
</span>
</Checkbox>
{bypassApproval && (
<FormControl
label="Reason for bypass"
className="mt-2"
isRequired
tooltipText="Enter a reason for bypassing the policy"
>
<span className="text-xs text-red">
Approve without waiting for requirements to be met (bypass policy
protection)
</span>
</Checkbox>
{bypassApproval && (
<FormControl
label="Reason for bypass"
className="mt-2"
isRequired
tooltipText="Enter a reason for bypassing the secret change policy"
>
<Input
value={bypassReason}
onChange={(e) => setBypassReason(e.currentTarget.value)}
placeholder="Enter reason for bypass (min 10 chars)"
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
/>
</FormControl>
)}
</div>
)}
<Input
value={bypassReason}
onChange={(e) => setBypassReason(e.currentTarget.value)}
placeholder="Enter reason for bypass (min 10 chars)"
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
/>
</FormControl>
)}
</div>
)}
<div className="space-x-2">
<Button
isLoading={isLoading === "approved"}

View File

@ -400,7 +400,7 @@ const Form = ({
isError={Boolean(error)}
tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path."
errorText={error?.message}
className="flex-grow"
className="flex-1"
>
<Select
isDisabled={isEditMode}
@ -419,6 +419,20 @@ const Form = ({
</FormControl>
)}
/>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Policy Name"
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
{!isAccessPolicyType && (
<Controller
control={control}
@ -429,7 +443,7 @@ const Form = ({
label="Min. Approvals Required"
isError={Boolean(error)}
errorText={error?.message}
className="flex-grow"
className="flex-shrink"
>
<Input
{...field}
@ -443,20 +457,6 @@ const Form = ({
)}
</div>
<div className="flex items-center gap-x-3">
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Policy Name"
isError={Boolean(error)}
errorText={error?.message}
className="flex-grow"
>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
@ -467,35 +467,36 @@ const Form = ({
label="Secret Path"
isError={Boolean(error)}
errorText={error?.message}
className="flex-grow"
className="flex-1"
>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isRequired
isError={Boolean(error)}
errorText={error?.message}
className="flex-1"
>
<FilterableSelect
isDisabled={isEditMode}
value={value}
onChange={onChange}
placeholder="Select environment..."
options={environments}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
</div>
<Controller
control={control}
name="environment"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
isDisabled={isEditMode}
value={value}
onChange={onChange}
placeholder="Select environment..."
options={environments}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
<div className="mb-2">
<p>Approvers</p>
<p className="font-inter text-xs text-mineshaft-300 opacity-90">
@ -504,11 +505,11 @@ const Form = ({
</div>
{isAccessPolicyType ? (
<>
<div className="thin-scrollbar max-h-64 space-y-2 overflow-y-auto rounded">
<div className="thin-scrollbar max-h-64 space-y-2 overflow-y-auto rounded border border-mineshaft-600 bg-mineshaft-900 p-2">
{sequenceApproversFieldArray.fields.map((el, index) => (
<div
className={twMerge(
"rounded border border-mineshaft-500 bg-mineshaft-700 p-3 pb-0",
"rounded border border-mineshaft-500 bg-mineshaft-700 p-3 pb-0 shadow-inner",
dragOverItem === index ? "border-2 border-blue-400" : "",
draggedItem === index ? "opacity-50" : ""
)}
@ -567,7 +568,7 @@ const Form = ({
label="User Approvers"
isError={Boolean(error)}
errorText={error?.message}
className="flex-grow"
className="flex-1"
>
<FilterableSelect
menuPortalTarget={modalContainer.current}
@ -597,7 +598,7 @@ const Form = ({
label="Group Approvers"
isError={Boolean(error)}
errorText={error?.message}
className="flex-grow"
className="flex-1"
>
<FilterableSelect
menuPortalTarget={modalContainer.current}
@ -800,10 +801,15 @@ const Form = ({
</>
)}
<div className="mt-8 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
<Button
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Save
</Button>
<Button onClick={() => onToggle(false)} variant="outline_bg">
<Button onClick={() => onToggle(false)} colorSchema="secondary" variant="plain">
Close
</Button>
</div>

View File

@ -102,43 +102,73 @@ export const SecretApprovalRequestAction = ({
if (!hasMerged && status === "open") {
return (
<div className="flex w-full flex-col items-start justify-between py-4 transition-all">
<div className="flex items-center space-x-4 px-4">
<div
className={`flex items-center justify-center rounded-full ${isMergable ? "h-10 w-10 bg-green" : "h-11 w-11 bg-red-600"}`}
>
<FontAwesomeIcon
icon={isMergable ? faCheck : faXmark}
className={isMergable ? "text-lg text-black" : "text-2xl text-white"}
/>
<div className="flex w-full flex-col items-start justify-between py-4 text-mineshaft-100 transition-all">
<div className="flex w-full flex-col justify-between xl:flex-row xl:items-center">
<div className="mr-auto flex items-center space-x-4 px-4">
<div
className={`flex items-center justify-center rounded-full ${isMergable ? "h-8 w-8 bg-green" : "h-10 w-10 bg-red-600"}`}
>
<FontAwesomeIcon
icon={isMergable ? faCheck : faXmark}
className={isMergable ? "text-lg text-white" : "text-2xl text-white"}
/>
</div>
<span className="flex flex-col">
<p className={`text-md font-medium ${isMergable && "text-lg"}`}>
{isMergable ? "Good to merge" : "Merging is blocked"}
</p>
{!isMergable && (
<span className="inline-block text-xs text-mineshaft-300">
At least {approvals} approving review{`${approvals > 1 ? "s" : ""}`} required by
eligible reviewers.
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
</span>
)}
</span>
</div>
<span className="flex flex-col">
<p className={`text-md font-medium ${isMergable && "text-lg"}`}>
{isMergable ? "Good to merge" : "Merging is blocked"}
</p>
{!isMergable && (
<span className="inline-block text-xs text-bunker-200">
At least {approvals} approving review{`${approvals > 1 ? "s" : ""}`} required by
eligible reviewers.
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
</span>
<div className="mt-4 flex items-center justify-end space-x-2 px-4 xl:mt-0">
{canApprove || isSoftEnforcement ? (
<div className="flex items-center space-x-4">
<Button
onClick={() => handleSecretApprovalStatusChange("close")}
isLoading={isStatusChanging}
variant="outline_bg"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faClose} />}
className="hover:border-red/60 hover:bg-red/10"
>
Close request
</Button>
<Button
leftIcon={<FontAwesomeIcon icon={!canApprove ? faLandMineOn : faCheck} />}
isDisabled={
!(
(isMergable && canApprove) ||
(isSoftEnforcement && byPassApproval && isValidBypassReason(bypassReason))
)
}
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}
colorSchema={isSoftEnforcement && !canApprove ? "danger" : "primary"}
variant="outline_bg"
>
Merge
</Button>
</div>
) : (
<div className="text-sm text-mineshaft-400">Only approvers can merge</div>
)}
</span>
</div>
</div>
{isSoftEnforcement && !isMergable && isBypasser && (
<div
className={`mt-4 w-full border-mineshaft-600 px-5 ${isMergable ? "border-t pb-2" : "border-y pb-4"}`}
>
<div className="mt-4 w-full border-t border-mineshaft-600 px-5">
<div className="mt-2 flex flex-col space-y-2 pt-2">
<Checkbox
onCheckedChange={(checked) => setByPassApproval(checked === true)}
isChecked={byPassApproval}
id="byPassApproval"
checkIndicatorBg="text-white"
className={twMerge(
"mr-2",
byPassApproval ? "border-red bg-red hover:bg-red-600" : ""
)}
className={twMerge("mr-2", byPassApproval ? "!border-red/30 !bg-red/10" : "")}
>
<span className="text-sm">
Merge without waiting for approval (bypass secret change policy)
@ -162,51 +192,18 @@ export const SecretApprovalRequestAction = ({
</div>
</div>
)}
<div className="mt-2 flex w-full items-center justify-end space-x-2 px-4">
{canApprove || isSoftEnforcement ? (
<div className="flex items-center space-x-4">
<Button
onClick={() => handleSecretApprovalStatusChange("close")}
isLoading={isStatusChanging}
variant="outline_bg"
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faClose} />}
className="hover:border-red/60 hover:bg-red/10"
>
Close request
</Button>
<Button
leftIcon={<FontAwesomeIcon icon={!canApprove ? faLandMineOn : faCheck} />}
isDisabled={
!(
(isMergable && canApprove) ||
(isSoftEnforcement && byPassApproval && isValidBypassReason(bypassReason))
)
}
isLoading={isMerging}
onClick={handleSecretApprovalRequestMerge}
colorSchema={isSoftEnforcement && !canApprove ? "danger" : "primary"}
variant="solid"
>
Merge
</Button>
</div>
) : (
<div>Only approvers can merge</div>
)}
</div>
</div>
);
}
if (hasMerged && status === "close")
return (
<div className="flex w-full items-center justify-between rounded-md border border-primary/60 bg-primary/10">
<div className="flex items-start space-x-4 p-4">
<FontAwesomeIcon icon={faCheck} className="pt-1 text-2xl text-primary" />
<div className="flex w-full items-center justify-between rounded-md border border-green/60 bg-green/10">
<div className="flex items-start space-x-2 p-4">
<FontAwesomeIcon icon={faCheck} className="mt-0.5 text-xl text-green" />
<span className="flex flex-col">
Change request merged
<span className="inline-block text-xs text-bunker-200">
<span className="inline-block text-xs text-mineshaft-300">
Merged by {statusChangeByEmail}.
</span>
</span>
@ -215,26 +212,26 @@ export const SecretApprovalRequestAction = ({
);
return (
<div className="flex w-full items-center justify-between">
<div className="flex items-start space-x-4">
<FontAwesomeIcon icon={faUserLock} className="pt-1 text-2xl text-primary" />
<div className="flex w-full items-center justify-between rounded-md border border-yellow/60 bg-yellow/10">
<div className="flex items-start space-x-2 p-4">
<FontAwesomeIcon icon={faUserLock} className="mt-0.5 text-xl text-yellow" />
<span className="flex flex-col">
Secret approval has been closed
<span className="inline-block text-xs text-bunker-200">
<span className="inline-block text-xs text-mineshaft-300">
Closed by {statusChangeByEmail}
</span>
</span>
</div>
<div className="flex items-center space-x-6">
<Button
onClick={() => handleSecretApprovalStatusChange("open")}
isLoading={isStatusChanging}
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
>
Reopen request
</Button>
</div>
<Button
onClick={() => handleSecretApprovalStatusChange("open")}
isLoading={isStatusChanging}
variant="plain"
colorSchema="secondary"
className="mr-4 text-yellow/60 hover:text-yellow"
leftIcon={<FontAwesomeIcon icon={faLockOpen} />}
>
Reopen request
</Button>
</div>
);
};

View File

@ -3,11 +3,12 @@
/* eslint-disable no-nested-ternary */
import { useState } from "react";
import {
faCircleCheck,
faCircleXmark,
faExclamationTriangle,
faEye,
faEyeSlash,
faInfo,
faInfoCircle,
faKey
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -29,14 +30,14 @@ export type Props = {
};
const generateItemTitle = (op: CommitType) => {
let text = { label: "", color: "" };
if (op === CommitType.CREATE) text = { label: "create", color: "#60DD00" };
else if (op === CommitType.UPDATE) text = { label: "change", color: "#F8EB30" };
else text = { label: "deletion", color: "#F83030" };
let text = { label: "", className: "" };
if (op === CommitType.CREATE) text = { label: "create", className: "text-green-600" };
else if (op === CommitType.UPDATE) text = { label: "change", className: "text-yellow-600" };
else text = { label: "deletion", className: "text-red-600" };
return (
<div className="text-md pb-2 font-medium">
Request for <span style={{ color: text.color }}>secret {text.label}</span>
Request for <span className={text.className}>secret {text.label}</span>
</div>
);
};
@ -68,15 +69,15 @@ export const SecretApprovalRequestChangeItem = ({
<div className="flex items-center px-1 py-1">
<div className="flex-grow">{generateItemTitle(op)}</div>
{!hasMerged && isStale && (
<div className="flex items-center">
<FontAwesomeIcon icon={faInfo} className="text-sm text-primary-600" />
<span className="ml-2 text-xs">Secret has been changed(stale)</span>
<div className="flex items-center text-mineshaft-300">
<FontAwesomeIcon icon={faInfoCircle} className="text-xs" />
<span className="ml-1 text-xs">Secret has been changed (stale)</span>
</div>
)}
{hasMerged && hasConflict && (
<div className="flex items-center space-x-2 text-sm text-bunker-300">
<div className="flex items-center space-x-1 text-xs text-bunker-300">
<Tooltip content="Merge Conflict">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red-700" />
<FontAwesomeIcon icon={faExclamationTriangle} className="text-xs text-red" />
</Tooltip>
<div>{generateConflictText(op)}</div>
</div>
@ -95,7 +96,7 @@ export const SecretApprovalRequestChangeItem = ({
</div>
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Key</div>
<div className="text-sm">{secretVersion?.secretKey} </div>
<p className="max-w-lg break-words text-sm">{secretVersion?.secretKey}</p>
</div>
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Value</div>
@ -147,7 +148,7 @@ export const SecretApprovalRequestChangeItem = ({
</div>
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Comment</div>
<div className="max-h-[5rem] overflow-y-auto text-sm">
<div className="thin-scrollbar max-h-[5rem] max-w-[34rem] overflow-y-auto break-words text-sm xl:max-w-[28rem]">
{secretVersion?.secretComment || (
<span className="text-sm text-mineshaft-300">-</span>
)}{" "}
@ -186,15 +187,27 @@ export const SecretApprovalRequestChangeItem = ({
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
<Tooltip
className="max-w-lg whitespace-normal break-words"
content={el.key}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.key}
</div>
</Tooltip>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
<Tooltip
className="max-w-lg whitespace-normal break-words"
content={el.value}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tooltip>
</Tag>
</div>
))}
@ -215,13 +228,13 @@ export const SecretApprovalRequestChangeItem = ({
<div className="mb-4 flex flex-row justify-between">
<span className="text-md font-medium">New Secret</span>
<div className="rounded-full bg-green-600 px-2 pb-[0.14rem] pt-[0.2rem] text-xs font-medium">
<FontAwesomeIcon icon={faCircleXmark} className="pr-1 text-white" />
<FontAwesomeIcon icon={faCircleCheck} className="pr-1 text-white" />
New
</div>
</div>
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Key</div>
<div className="text-sm">{newVersion?.secretKey} </div>
<div className="max-w-md break-words text-sm">{newVersion?.secretKey} </div>
</div>
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Value</div>
@ -273,7 +286,7 @@ export const SecretApprovalRequestChangeItem = ({
</div>
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Comment</div>
<div className="max-h-[5rem] overflow-y-auto text-sm">
<div className="thin-scrollbar max-h-[5rem] max-w-[34rem] overflow-y-auto break-words text-sm xl:max-w-[28rem]">
{newVersion?.secretComment || (
<span className="text-sm text-mineshaft-300">-</span>
)}{" "}
@ -281,15 +294,15 @@ export const SecretApprovalRequestChangeItem = ({
</div>
<div className="mb-2">
<div className="text-sm font-medium text-mineshaft-300">Tags</div>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-y-2">
{(newVersion?.tags?.length ?? 0) ? (
newVersion?.tags?.map(({ slug, id: tagId, color }) => (
<Tag
className="flex w-min items-center space-x-2"
className="flex w-min items-center space-x-1.5 border border-mineshaft-500 bg-mineshaft-800"
key={`${newVersion.id}-${tagId}`}
>
<div
className="h-3 w-3 rounded-full"
className="h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: color || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
@ -311,15 +324,27 @@ export const SecretApprovalRequestChangeItem = ({
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
<Tooltip
className="max-w-lg whitespace-normal break-words"
content={el.key}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.key}
</div>
</Tooltip>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
<Tooltip
className="max-w-lg whitespace-normal break-words"
content={el.value}
>
<div className="max-w-[125px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tooltip>
</Tag>
</div>
))}

View File

@ -3,12 +3,12 @@ import { Controller, useForm } from "react-hook-form";
import {
faAngleDown,
faArrowLeft,
faCheckCircle,
faCircle,
faBan,
faCheck,
faCodeBranch,
faComment,
faFolder,
faXmarkCircle
faHourglass
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@ -26,6 +26,7 @@ import {
DropdownMenuTrigger,
EmptyState,
FormControl,
GenericFieldLabel,
IconButton,
TextArea,
Tooltip
@ -81,10 +82,10 @@ export const generateCommitText = (commits: { op: CommitType }[] = [], isReplica
const getReviewedStatusSymbol = (status?: ApprovalStatus) => {
if (status === ApprovalStatus.APPROVED)
return <FontAwesomeIcon icon={faCheckCircle} size="xs" style={{ color: "#15803d" }} />;
return <FontAwesomeIcon icon={faCheck} size="xs" className="text-green" />;
if (status === ApprovalStatus.REJECTED)
return <FontAwesomeIcon icon={faXmarkCircle} size="xs" style={{ color: "#b91c1c" }} />;
return <FontAwesomeIcon icon={faCircle} size="xs" style={{ color: "#c2410c" }} />;
return <FontAwesomeIcon icon={faBan} size="xs" className="text-red" />;
return <FontAwesomeIcon icon={faHourglass} size="xs" className="text-yellow" />;
};
type Props = {
@ -223,8 +224,8 @@ export const SecretApprovalRequestChanges = ({
const hasMerged = secretApprovalRequestDetails?.hasMerged;
return (
<div className="flex space-x-6">
<div className="flex-grow">
<div className="flex flex-col space-x-6 lg:flex-row">
<div className="flex-1 lg:max-w-[calc(100%-17rem)]">
<div className="sticky top-0 z-20 flex items-center space-x-4 bg-bunker-800 pb-6 pt-2">
<IconButton variant="outline_bg" ariaLabel="go-back" onClick={onGoBack}>
<FontAwesomeIcon icon={faArrowLeft} />
@ -242,17 +243,17 @@ export const SecretApprovalRequestChanges = ({
: secretApprovalRequestDetails.status}
</span>
</div>
<div className="flex-grow flex-col">
<div className="-mt-0.5 flex-grow flex-col">
<div className="text-xl">
{generateCommitText(
secretApprovalRequestDetails.commits,
secretApprovalRequestDetails.isReplicated
)}
</div>
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="-mt-1 flex items-center space-x-2 text-xs text-gray-400">
By {secretApprovalRequestDetails?.committerUser?.firstName} (
{secretApprovalRequestDetails?.committerUser?.email})
</div>
</span>
</div>
{!hasMerged &&
secretApprovalRequestDetails.status === "open" &&
@ -262,7 +263,10 @@ export const SecretApprovalRequestChanges = ({
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
>
<DropdownMenuTrigger asChild>
<Button rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}>
<Button
colorSchema="secondary"
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
>
Review
</Button>
</DropdownMenuTrigger>
@ -279,82 +283,87 @@ export const SecretApprovalRequestChanges = ({
{...field}
placeholder="Leave a comment..."
reSize="none"
className="text-md mt-2 h-40 border border-mineshaft-600 bg-bunker-800"
className="text-md mt-2 h-40 border border-mineshaft-600 bg-mineshaft-800 placeholder:text-mineshaft-400"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="status"
defaultValue={ApprovalStatus.APPROVED}
render={({ field, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="mb-4 space-y-2"
aria-label="Status"
<div className="flex justify-between">
<Controller
control={control}
name="status"
defaultValue={ApprovalStatus.APPROVED}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-0"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="flex items-center gap-2">
<RadioGroupItem
id="approve"
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.APPROVED}
aria-labelledby="approve-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="approve-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.APPROVED);
}
}}
tabIndex={0}
role="button"
>
Approve
</span>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem
id="reject"
className="h-4 w-4 rounded-full border border-gray-300 text-red focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.REJECTED}
aria-labelledby="reject-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="reject-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.REJECTED);
}
}}
tabIndex={0}
role="button"
>
Reject
</span>
</div>
</RadioGroup>
</FormControl>
)}
/>
<div className="flex justify-end">
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="space-y-2"
aria-label="Status"
>
<div className="flex items-center gap-2">
<RadioGroupItem
id="approve"
className="h-4 w-4 rounded-full border border-gray-400 text-green focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.APPROVED}
aria-labelledby="approve-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="approve-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.APPROVED);
}
}}
tabIndex={0}
role="button"
>
Approve
</span>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem
id="reject"
className="h-4 w-4 rounded-full border border-gray-400 text-red focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.REJECTED}
aria-labelledby="reject-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="reject-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.REJECTED);
}
}}
tabIndex={0}
role="button"
>
Reject
</span>
</div>
</RadioGroup>
</FormControl>
)}
/>
<Button
type="submit"
isLoading={isApproving || isRejecting || isSubmitting}
variant="outline_bg"
className="mt-auto h-min"
>
Submit Review
</Button>
@ -371,14 +380,14 @@ export const SecretApprovalRequestChanges = ({
<div className="text-sm text-bunker-300">
A secret import in
<p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
className="mx-1 inline rounded bg-mineshaft-600/80 text-mineshaft-300"
style={{ padding: "2px 4px" }}
>
{secretApprovalRequestDetails?.environment}
</p>
<div className="mr-2 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
<div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1.5 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1.5">
<FontAwesomeIcon icon={faFolder} className="text-yellow" size="sm" />
</p>
<Tooltip content={approvalSecretPath}>
<p
@ -391,14 +400,14 @@ export const SecretApprovalRequestChanges = ({
</div>
has pending changes to be accepted from its source at{" "}
<p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
className="mx-1 inline rounded bg-mineshaft-600/80 text-mineshaft-300"
style={{ padding: "2px 4px" }}
>
{replicatedImport?.importEnv?.slug}
</p>
<div className="inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
<div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1.5 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1.5">
<FontAwesomeIcon icon={faFolder} className="text-yellow" size="sm" />
</p>
<Tooltip content={replicatedImport?.importPath}>
<p
@ -415,14 +424,14 @@ export const SecretApprovalRequestChanges = ({
<div className="text-sm text-bunker-300">
<p className="inline">Secret(s) in</p>
<p
className="mx-1 inline rounded bg-primary-600/40 text-primary-300"
className="mx-1 inline rounded bg-mineshaft-600/80 text-mineshaft-300"
style={{ padding: "2px 4px" }}
>
{secretApprovalRequestDetails?.environment}
</p>
<div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1">
<FontAwesomeIcon icon={faFolder} className="text-primary" size="sm" />
<div className="mr-1 inline-flex w-min items-center rounded border border-mineshaft-500 pl-1.5 pr-2">
<p className="cursor-default border-r border-mineshaft-500 pr-1.5">
<FontAwesomeIcon icon={faFolder} className="text-yellow" size="sm" />
</p>
<Tooltip content={formatReservedPaths(secretApprovalRequestDetails.secretPath)}>
<p
@ -463,7 +472,7 @@ export const SecretApprovalRequestChanges = ({
const reviewer = reviewedUsers?.[requiredApprover.userId];
return (
<div
className="flex w-full flex-col rounded-md bg-mineshaft-800 p-4"
className="flex w-full flex-col rounded-md bg-mineshaft-800 p-4 text-sm text-mineshaft-100"
key={`required-approver-${requiredApprover.userId}`}
>
<div>
@ -477,14 +486,16 @@ export const SecretApprovalRequestChanges = ({
{reviewer?.status === ApprovalStatus.APPROVED ? "approved" : "rejected"}
</span>{" "}
the request on{" "}
{format(new Date(secretApprovalRequestDetails.createdAt), "PPpp zzz")}.
{format(
new Date(secretApprovalRequestDetails.createdAt),
"MM/dd/yyyy h:mm:ss aa"
)}
.
</div>
{reviewer?.comment && (
<FormControl label="Comment" className="mb-0 mt-4">
<TextArea value={reviewer.comment} isDisabled reSize="none">
{reviewer?.comment && reviewer.comment}
</TextArea>
</FormControl>
<GenericFieldLabel label="Comment" className="mt-2 max-w-4xl break-words">
{reviewer?.comment && reviewer.comment}
</GenericFieldLabel>
)}
</div>
);
@ -505,7 +516,7 @@ export const SecretApprovalRequestChanges = ({
/>
</div>
</div>
<div className="sticky top-0 w-1/5 cursor-default pt-4" style={{ minWidth: "240px" }}>
<div className="sticky top-0 z-[51] w-1/5 cursor-default pt-2" style={{ minWidth: "240px" }}>
<div className="text-sm text-bunker-300">Reviewers</div>
<div className="mt-2 flex flex-col space-y-2 text-sm">
{secretApprovalRequestDetails?.policy?.approvers
@ -526,17 +537,17 @@ export const SecretApprovalRequestChanges = ({
requiredApprover.lastName || ""
}`}
>
<span>{requiredApprover?.email} </span>
<span>{requiredApprover?.email}</span>
</Tooltip>
<span className="text-red">*</span>
</div>
<div>
{reviewer?.comment && (
<Tooltip content={reviewer.comment}>
<Tooltip className="max-w-lg break-words" content={reviewer.comment}>
<FontAwesomeIcon
icon={faComment}
size="xs"
className="mr-1 text-mineshaft-300"
className="mr-1.5 text-mineshaft-300"
/>
</Tooltip>
)}

View File

@ -7,8 +7,13 @@ type Props = {
export const OnePassSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { vaultId }
destinationConfig: { vaultId, valueLabel }
} = secretSync;
return <GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>;
return (
<>
<GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>
<GenericFieldLabel label="Value Key">{valueLabel || "value"}</GenericFieldLabel>
</>
);
};

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'
@ -607,6 +608,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',
@ -2353,6 +2360,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'
@ -4484,6 +4498,7 @@ interface adminLayoutRouteChildren {
adminAuthenticationPageRouteRoute: typeof adminAuthenticationPageRouteRoute
adminCachingPageRouteRoute: typeof adminCachingPageRouteRoute
adminEncryptionPageRouteRoute: typeof adminEncryptionPageRouteRoute
adminEnvironmentPageRouteRoute: typeof adminEnvironmentPageRouteRoute
adminIntegrationsPageRouteRoute: typeof adminIntegrationsPageRouteRoute
adminMachineIdentitiesResourcesPageRouteRoute: typeof adminMachineIdentitiesResourcesPageRouteRoute
adminOrganizationResourcesPageRouteRoute: typeof adminOrganizationResourcesPageRouteRoute
@ -4495,6 +4510,7 @@ const adminLayoutRouteChildren: adminLayoutRouteChildren = {
adminAuthenticationPageRouteRoute: adminAuthenticationPageRouteRoute,
adminCachingPageRouteRoute: adminCachingPageRouteRoute,
adminEncryptionPageRouteRoute: adminEncryptionPageRouteRoute,
adminEnvironmentPageRouteRoute: adminEnvironmentPageRouteRoute,
adminIntegrationsPageRouteRoute: adminIntegrationsPageRouteRoute,
adminMachineIdentitiesResourcesPageRouteRoute:
adminMachineIdentitiesResourcesPageRouteRoute,
@ -4697,6 +4713,7 @@ export interface FileRoutesByFullPath {
'/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute
'/admin/environment': typeof adminEnvironmentPageRouteRoute
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
@ -4918,6 +4935,7 @@ export interface FileRoutesByTo {
'/admin/authentication': typeof adminAuthenticationPageRouteRoute
'/admin/caching': typeof adminCachingPageRouteRoute
'/admin/encryption': typeof adminEncryptionPageRouteRoute
'/admin/environment': typeof adminEnvironmentPageRouteRoute
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
@ -5139,6 +5157,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/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
@ -5371,6 +5390,7 @@ export interface FileRouteTypes {
| '/admin/authentication'
| '/admin/caching'
| '/admin/encryption'
| '/admin/environment'
| '/admin/integrations'
| '/cert-manager/$projectId'
| '/kms/$projectId'
@ -5591,6 +5611,7 @@ export interface FileRouteTypes {
| '/admin/authentication'
| '/admin/caching'
| '/admin/encryption'
| '/admin/environment'
| '/admin/integrations'
| '/cert-manager/$projectId'
| '/kms/$projectId'
@ -5810,6 +5831,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/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
@ -6267,6 +6289,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",
@ -6309,6 +6332,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"),